AutomationFlowsWeb Scraping › Auto-sync Local Events to Google Calendar with N8n

Auto-sync Local Events to Google Calendar with N8n

ByYaron Been @yaron-nofluff on n8n.io

This workflow automates the process of finding local events and adding them directly to your Google Calendar. It eliminates the need for manual event tracking by automatically scraping event information and creating calendar entries.

Cron / scheduled trigger★★★★☆ complexity11 nodesHTTP RequestGoogle Calendar
Web Scraping Trigger: Cron / scheduled Nodes: 11 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #5216 — we link there as the canonical source.

This workflow follows the Google Calendar → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "y1ZOHX6Zq13C68dP",
  "name": "AutoSync_Local_Events_to_Google_Calendar",
  "tags": [],
  "nodes": [
    {
      "id": "433e4192-599f-4f3d-a251-651b81db4960",
      "name": "Daily Event Sync Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2740,
        220
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "94f20c26-9a90-43ba-8ab1-b1107c17345c",
      "name": "Fetch Event Page (Bright Data)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2520,
        220
      ],
      "parameters": {
        "url": "https://api.brightdata.com/request",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "zone",
              "value": "n8n_unblocker"
            },
            {
              "name": "url",
              "value": "https://www.nypl.org/events/calendar"
            },
            {
              "name": "country",
              "value": "us"
            },
            {
              "name": "format",
              "value": "raw"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e55c8584-f673-4201-bda7-42cdbadf49bf",
      "name": "Extract Event Data (HTML Parser)",
      "type": "n8n-nodes-base.html",
      "position": [
        -2220,
        220
      ],
      "parameters": {
        "options": {},
        "operation": "extractHtmlContent",
        "extractionValues": {
          "values": [
            {
              "key": "Title",
              "cssSelector": ".event-title",
              "returnArray": true
            },
            {
              "key": "Location",
              "cssSelector": ".event-location",
              "returnArray": true
            },
            {
              "key": "Audience",
              "cssSelector": ".event-audience",
              "returnArray": true
            },
            {
              "key": "Time",
              "cssSelector": ".event-time",
              "returnArray": true
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "48196549-0378-4cd9-8e1d-f289c4b50180",
      "name": "Clean & Format Event Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -2000,
        220
      ],
      "parameters": {
        "jsCode": "const data = items[0].json;\n\n// Extract arrays\nconst titles = data.Title || [];\nconst locations = data.Location || [];\nconst audiences = data.Audience || [];\nconst rawTimes = data.Time || [];\n\n// Step 1: Remove invalid \"time\" placeholders\nconst invalidTimeLabels = [\"Date/Time\", \"Title/Description\", \"Location\", \"Audience\"];\nconst times = rawTimes.filter(time => !invalidTimeLabels.includes(time.trim()));\n\n// Step 2: Safely calculate number of valid events\nconst eventCount = Math.min(titles.length, locations.length, audiences.length, times.length);\n\n// Helper: Convert \"Today @ 10 AM\" \u2192 ISO string with timezone\nfunction parseTimeToISO(rawTime) {\n  const match = rawTime.match(/@ ([0-9]{1,2})(?::([0-9]{2}))?\\s?(AM|PM)/i);\n  const now = new Date();\n\n  if (!match) return null;\n\n  let hour = parseInt(match[1]);\n  const minute = match[2] ? parseInt(match[2]) : 0;\n  const meridian = match[3].toUpperCase();\n\n  if (meridian === \"PM\" && hour !== 12) hour += 12;\n  if (meridian === \"AM\" && hour === 12) hour = 0;\n\n  const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute);\n  const end = new Date(start.getTime() + 60 * 60 * 1000); // 1-hour event\n\n  const offsetMinutes = start.getTimezoneOffset();\n  const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);\n  const offsetMins = Math.abs(offsetMinutes) % 60;\n  const offsetSign = offsetMinutes > 0 ? \"-\" : \"+\";\n  const offset = `${offsetSign}${String(offsetHours).padStart(2, \"0\")}:${String(offsetMins).padStart(2, \"0\")}`;\n\n  const toISOStringWithOffset = (d) => {\n    const pad = (n) => n.toString().padStart(2, \"0\");\n    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:00${offset}`;\n  };\n\n  return {\n    start: toISOStringWithOffset(start),\n    end: toISOStringWithOffset(end),\n  };\n}\n\n// Step 3: Build cleaned and enriched event objects\nconst results = [];\n\nfor (let i = 0; i < eventCount; i++) {\n  const titleText = titles[i];\n  const timeText = times[i]?.trim();\n  const parsedTime = parseTimeToISO(timeText);\n\n  results.push({\n    json: {\n      title: titleText.split('\\n')[0]?.trim(),\n      description: titleText.trim(),\n      location: locations[i]?.trim(),\n      audience: audiences[i]?.trim(),\n      time: timeText,\n      start: { dateTime: parsedTime?.start || null },\n      end: { dateTime: parsedTime?.end || null },\n      sourceUrl: \"https://www.nypl.org\" + (titleText.match(/\\[([^\\]]+)\\]/)?.[1] || '')\n    }\n  });\n}\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "945e912a-609a-42e0-82a4-c3d36623a767",
      "name": "Create Google Calendar Events",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        -1700,
        220
      ],
      "parameters": {
        "end": "={{ $json.end.dateTime }}",
        "start": "={{ $json.start.dateTime }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "user@example.com",
          "cachedResultName": "Community Events"
        },
        "additionalFields": {
          "attendees": [],
          "description": "=Title: {{ $json.title }}\nDescription: {{ $json.description }}\nLocation: {{ $json.location }}\nAudience: {{ $json.audience }}\nTime: {{ $json.time }}"
        }
      },
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "6c5825a7-6a1e-485c-820b-ed4188c6b718",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2800,
        -540
      ],
      "parameters": {
        "color": 5,
        "width": 440,
        "height": 980,
        "content": "## \ud83e\udded **Section 1: Event Data Fetcher**\n\n### \ud83d\udd01 `\ud83d\udd52 Daily Event Sync Trigger`\n\n> **Node:** *Schedule Trigger*\n> \u23f0 This node sets **when** your automation runs. For example, you might want it to run every morning at 8:00 AM to check for new community events.\n\n\ud83d\udd0d Why it matters:\n\n> This ensures your public calendar is always up to date without manual work.\n\n---\n\n### \ud83c\udf10 `\ud83c\udf10 Fetch Event Page (Bright Data)`\n\n> **Node:** *HTTP Request using Bright Data Web Unlocker*\n> \ud83d\udee1 This node sends a request to a **local government or library event page** using Bright Data\u2019s **Web Unlocker proxy**, which helps bypass blocks or bot detection.\n\n\ud83d\udd27 What\u2019s special here:\n\n* It uses your **Bright Data proxy zone**\n* It targets a public events URL (e.g., a `.gov` or `.org`)\n* Makes sure you get the raw HTML page \u2014 even if the site tries to hide it\n\n\u2705 Benefit:\n\n> This lets you scrape data reliably from websites that are normally hard to reach for bots.\n\n---\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "aa5624b4-6c2e-4d70-a05b-4e038ee82dfb",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2300,
        -660
      ],
      "parameters": {
        "color": 3,
        "width": 440,
        "height": 1100,
        "content": "## \ud83e\udde9 **Section 2: Smart Event Extractor**\n\n### \ud83e\udde9 `\ud83e\udde9 Extract Event Data (HTML Parser)`\n\n> **Node:** *HTML*\n> \ud83d\udcc4 This node parses the raw HTML response from the website and extracts useful information like:\n\n* \ud83c\udff7 Event Titles\n* \ud83d\udccd Locations\n* \ud83d\udc65 Target Audience\n* \u23f0 Raw Times\n\nIt uses selectors (like CSS) to grab content from specific parts of the page.\n\n\ud83c\udfaf Goal:\n\n> Transform messy website code into **structured data** you can actually use.\n\n---\n\n### \ud83e\udde0 `\ud83e\udde0 Clean & Format Event Data`\n\n> **Node:** *Code*\n> \ud83e\uddfc This is the brain of your workflow. It takes the raw extracted data and:\n\n* Matches titles with their correct time/location\n* Skips garbage data like `\"Date/Time\"` headers\n* \ud83e\uddee Converts time to proper ISO format (like `2025-06-21T11:00:00-07:00`) for Google Calendar\n\n\ud83d\ude80 Bonus:\n\n> This ensures only **valid**, well-structured events are passed forward \u2014 nothing broken, empty, or misaligned.\n\n---\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "b19c44ba-1abb-4d0b-a854-1d844163ffdf",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1800,
        -240
      ],
      "parameters": {
        "color": 6,
        "width": 320,
        "height": 680,
        "content": "## \ud83d\udcc5 **Section 3: Auto Calendar Creator**\n\n### \ud83d\udcc5 `\ud83d\udcc5 Create Google Calendar Events`\n\n> **Node:** *Google Calendar*\n> \ud83e\uddd9\u200d\u2642\ufe0f This node automatically **creates public calendar events** using the cleaned-up data.\n\nHere\u2019s what it sets:\n\n* **Summary:** Event title\n* **Description:** Full details (e.g., location, what it\u2019s about)\n* **Start/End Time:** In the correct Google Calendar format\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "85488a2c-a51a-4561-8f7d-44901a3a04a1",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4600,
        -500
      ],
      "parameters": {
        "color": 4,
        "width": 1300,
        "height": 320,
        "content": "=======================================\n            WORKFLOW ASSISTANCE\n=======================================\nFor any questions or support, please contact:\n    Yaron@nofluff.online\n\nExplore more tips and tutorials here:\n   - YouTube: https://www.youtube.com/@YaronBeen/videos\n   - LinkedIn: https://www.linkedin.com/in/yaronbeen/\n=======================================\n"
      },
      "typeVersion": 1
    },
    {
      "id": "2395bf91-efa9-4149-b411-38d18924c5c5",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4600,
        -160
      ],
      "parameters": {
        "color": 4,
        "width": 1289,
        "height": 2298,
        "content": "# **\ud83d\udcc5 AutoSync Local Events to Google Calendar**\n\n### *A No-Code Automation to Scrape, Clean, and Publish Community Events*\n\n---\n## \ud83e\udded **Section 1: Event Data Fetcher**\n\n### \ud83d\udd01 `\ud83d\udd52 Daily Event Sync Trigger`\n\n> **Node:** *Schedule Trigger*\n> \u23f0 This node sets **when** your automation runs. For example, you might want it to run every morning at 8:00 AM to check for new community events.\n\n\ud83d\udd0d Why it matters:\n\n> This ensures your public calendar is always up to date without manual work.\n\n---\n\n### \ud83c\udf10 `\ud83c\udf10 Fetch Event Page (Bright Data)`\n\n> **Node:** *HTTP Request using Bright Data Web Unlocker*\n> \ud83d\udee1 This node sends a request to a **local government or library event page** using Bright Data\u2019s **Web Unlocker proxy**, which helps bypass blocks or bot detection.\n\n\ud83d\udd27 What\u2019s special here:\n\n* It uses your **Bright Data proxy zone**\n* It targets a public events URL (e.g., a `.gov` or `.org`)\n* Makes sure you get the raw HTML page \u2014 even if the site tries to hide it\n\n\u2705 Benefit:\n\n> This lets you scrape data reliably from websites that are normally hard to reach for bots.\n\n---\n\n## \ud83e\udde9 **Section 2: Smart Event Extractor**\n\n### \ud83e\udde9 `\ud83e\udde9 Extract Event Data (HTML Parser)`\n\n> **Node:** *HTML*\n> \ud83d\udcc4 This node parses the raw HTML response from the website and extracts useful information like:\n\n* \ud83c\udff7 Event Titles\n* \ud83d\udccd Locations\n* \ud83d\udc65 Target Audience\n* \u23f0 Raw Times\n\nIt uses selectors (like CSS) to grab content from specific parts of the page.\n\n\ud83c\udfaf Goal:\n\n> Transform messy website code into **structured data** you can actually use.\n\n---\n\n### \ud83e\udde0 `\ud83e\udde0 Clean & Format Event Data`\n\n> **Node:** *Code*\n> \ud83e\uddfc This is the brain of your workflow. It takes the raw extracted data and:\n\n* Matches titles with their correct time/location\n* Skips garbage data like `\"Date/Time\"` headers\n* \ud83e\uddee Converts time to proper ISO format (like `2025-06-21T11:00:00-07:00`) for Google Calendar\n\n\ud83d\ude80 Bonus:\n\n> This ensures only **valid**, well-structured events are passed forward \u2014 nothing broken, empty, or misaligned.\n\n---\n\n## \ud83d\udcc5 **Section 3: Auto Calendar Creator**\n\n### \ud83d\udcc5 `\ud83d\udcc5 Create Google Calendar Events`\n\n> **Node:** *Google Calendar*\n> \ud83e\uddd9\u200d\u2642\ufe0f This node automatically **creates public calendar events** using the cleaned-up data.\n\nHere\u2019s what it sets:\n\n* **Summary:** Event title\n* **Description:** Full details (e.g., location, what it\u2019s about)\n* **Start/End Time:** In the correct Google Calendar format\n\n\ud83e\udef6 Why this matters:\n\n> Now your scraped community events are published straight to a **Google Calendar** \u2014 which you can embed on a website, share with citizens, or subscribe to.\n\n---\n\n## \ud83e\udde9 Summary: Why This Workflow Rocks\n\n| \ud83d\udd27 Step              | \ud83d\udccc What It Does                                       | \ud83c\udf81 Benefit to You                            |\n| -------------------- | ----------------------------------------------------- | -------------------------------------------- |\n| **Schedule Trigger** | Starts the automation daily                           | Keeps your calendar always fresh             |\n| **HTTP Request**     | Grabs raw event info from a gov/library site          | Reliable access to public event data         |\n| **HTML Parser**      | Pulls structured info from messy HTML                 | Turns code into useful content               |\n| **Code Node**        | Fixes formatting, converts time, aligns titles & more | Clean data ready for use                     |\n| **Calendar Node**    | Publishes events into your Google Calendar            | Fully automated public calendar \u2014 no effort! |\n\n---\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "f4286f93-9457-4042-9b4d-bbb8304eaac1",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1420,
        -240
      ],
      "parameters": {
        "color": 7,
        "width": 380,
        "height": 240,
        "content": "## I\u2019ll receive a tiny commission if you join Bright Data through this link\u2014thanks for fueling more free content!\n\n### https://get.brightdata.com/1tndi4600b25"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "69086b47-9272-4887-8844-db532999a0ba",
  "connections": {
    "Daily Event Sync Trigger": {
      "main": [
        [
          {
            "node": "Fetch Event Page (Bright Data)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean & Format Event Data": {
      "main": [
        [
          {
            "node": "Create Google Calendar Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Event Page (Bright Data)": {
      "main": [
        [
          {
            "node": "Extract Event Data (HTML Parser)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Event Data (HTML Parser)": {
      "main": [
        [
          {
            "node": "Clean & Format Event Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow automates the process of finding local events and adding them directly to your Google Calendar. It eliminates the need for manual event tracking by automatically scraping event information and creating calendar entries.

Source: https://n8n.io/workflows/5216/ — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

[TEMPLATE] Full Class -> Calendar Sync. Uses httpRequest, googleCalendar. Scheduled trigger; 24 nodes.

HTTP Request, Google Calendar
Web Scraping

Teams that track absences in Everhour and want a shared Google Calendar view for quick planning. Ideal for managers, HR/OPS, and teammates who need instant visibility into approved time off. Pulls app

HTTP Request, Google Calendar
Web Scraping

🕌 How it works

Google Calendar, HTTP Request
Web Scraping

Import Forex Factory Calendar events into Google Calendar. Delete past Forex Factory Calendar events from Google Calendar. Get reminders for important economic data releases — especially High Impact n

HTTP Request, Google Calendar
Web Scraping

Simple Calendar Sync. Uses googleCalendar, httpRequest. Scheduled trigger; 4 nodes.

Google Calendar, HTTP Request