{
  "nodes": [
    {
      "id": "fd27db75-494d-4e71-9dfd-a0e042a123fb",
      "name": "\ud83d\udccb MAIN \u2014 Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        -1072
      ],
      "parameters": {
        "width": 760,
        "height": 960,
        "content": "## \ud83d\udcc5 Event Promotion \u2014 Auto-Generate Graphic & Schedule Posts Across Social Channels\n\n**What this workflow does:**\nWhen a new event is added to Google Calendar (or Eventbrite via polling), this workflow:\n1. Extracts event details (name, date, time, location, description)\n2. Uses the **Edit Image node** to programmatically compose a branded promotional graphic (overlaying event title + date onto a template image)\n3. Uploads the graphic via **UploadToURL** to get a public CDN URL\n4. Uses a **DateTime node** to calculate the optimal post schedule (48hrs, 24hrs, and 1hr before event)\n5. Loops through 3 scheduled time slots using **Loop Over Items**\n6. At each loop iteration, a **Wait node** pauses until the scheduled time, then fires posts to LinkedIn, Twitter/X, and Facebook simultaneously\n7. Logs every post action to **Google Sheets** (platform, post time, event name, status)\n8. Sends a **Telegram** confirmation message to the admin after all posts are dispatched\n\n**Architecture (fully unique from Templates 1 & 2):**\n- \ud83d\udcc5 Google Calendar Trigger (NOT RSS / Schedule / Webhook)\n- \ud83d\uddbc\ufe0f Edit Image Node \u2014 programmatic graphic composition\n- \u2601\ufe0f UploadToURL \u2014 mandatory CDN hosting of generated graphic\n- \ud83d\udd50 DateTime Node \u2014 calculates 3 staggered post schedules\n- \ud83d\udd01 Loop Over Items \u2014 iterates through each scheduled slot\n- \u23f3 Wait Node \u2014 pauses execution until exact post time\n- \ud83d\udd00 Merge Node \u2014 recombines event data + schedule data\n- \ud83d\udcca Google Sheets \u2014 post audit log\n- \ud83d\udce8 Telegram \u2014 admin notification after completion\n\n**Setup Requirements:**\n1. Google Calendar OAuth2 credentials (watch a specific calendar)\n2. A base event banner image stored at a public URL (used as graphic template)\n3. UploadToURL endpoint configured\n4. LinkedIn OAuth2 credentials\n5. Twitter/X OAuth1 credentials\n6. Facebook Graph API token (pages_manage_posts scope)\n7. Google Sheets OAuth2 \u2014 create a sheet named `EventPostLog` with columns: EventID, EventName, Platform, ScheduledTime, PostedAt, Status, ImageURL\n8. Telegram Bot Token + your Chat ID"
      },
      "typeVersion": 1
    },
    {
      "id": "0ec5b6a5-821f-4ffa-8fe1-ff30bcc481bd",
      "name": "\ud83d\udcdd Note \u2014 Calendar Trigger & Event Parse",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        832,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 644,
        "height": 612,
        "content": "### \ud83d\udcc5 Step 1 \u2014 Google Calendar Trigger & Event Data Extraction\n**Google Calendar Trigger:** Fires instantly when any new event is created in your watched calendar. No polling \u2014 uses Google's push notification system.\n**Code Node (Parse Event):** Extracts `eventId`, `summary` (title), `description`, `location`, `startDateTime`, `endDateTime`, and the event organizer. Also derives a clean `eventDate` string and `daysUntilEvent` count used for scheduling logic downstream.\n**IF Node (Future Events Only):** Skips events that start in less than 2 hours \u2014 not enough lead time to schedule meaningful promotional posts."
      },
      "typeVersion": 1
    },
    {
      "id": "417e25e5-70d9-48a6-a0c0-9d927fa92664",
      "name": "\ud83d\udcdd Note \u2014 Edit Image, Upload & Schedule Calc",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1488,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 884,
        "height": 612,
        "content": "### \ud83d\uddbc\ufe0f Step 2 \u2014 Graphic Generation & UploadToURL\n**HTTP \u2014 Fetch Banner Template:** Downloads your base event banner image (stored at a fixed public URL) as binary.\n**Edit Image Node:** Overlays the event title, date, and location as text onto the banner image. Uses built-in composite operations \u2014 no external design API needed.\n**UploadToURL:** Uploads the composed graphic binary to your CDN. Returns a permanent public URL used in all social posts across all scheduled slots.\n**DateTime Node:** Calculates exactly 3 post timestamps \u2014 48 hours before, 24 hours before, and 1 hour before the event start time."
      },
      "typeVersion": 1
    },
    {
      "id": "55c3090d-cf83-4c86-afa6-9d5f48ab2a12",
      "name": "\ud83d\udcdd Note \u2014 Merge, Loop, Wait & Post",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2384,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 1060,
        "height": 612,
        "content": "### \ud83d\udd01 Step 3 \u2014 Loop, Wait & Scheduled Multi-Platform Posting\n**Merge Node:** Joins the event data (from step 1) with the scheduled timestamps + hosted image URL (from step 2) into a single enriched item before looping.\n**Loop Over Items:** Iterates over the 3 schedule slots one by one (48hr, 24hr, 1hr).\n**Wait Node:** Inside each loop iteration, pauses workflow execution until the exact scheduled timestamp. n8n resumes automatically \u2014 no cron job needed.\n**OpenAI Node:** Generates a time-aware caption (e.g. 'Event tomorrow!' vs 'Starting in 1 hour!') based on which slot is being processed.\n**3 Parallel Posts:** LinkedIn, Twitter/X, and Facebook all post simultaneously within each loop iteration."
      },
      "typeVersion": 1
    },
    {
      "id": "3c381e4b-341c-46f9-ac52-bd9ab269c2f3",
      "name": "\ud83d\udcdd Note \u2014 Sheets Log & Telegram Notify",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3472,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 820,
        "height": 928,
        "content": "### \ud83d\udcca Step 4 \u2014 Google Sheets Log & Telegram Notification\n**Google Sheets \u2014 Append Row:** After each post within the loop, appends a row to `EventPostLog` with EventID, EventName, Platform, ScheduledTime, PostedAt, Status, and ImageURL \u2014 giving a full audit trail of every promotional post.\n**Telegram \u2014 Admin Alert:** After all 3 loop iterations complete (all 9 posts dispatched \u2014 3 slots \u00d7 3 platforms), sends a Telegram message to the admin summarising the event name, how many posts were made, and the hosted graphic URL."
      },
      "typeVersion": 1
    },
    {
      "id": "aeda65f6-4d8a-443b-8dac-aa0d6f361ce3",
      "name": "Google Calendar \u2014 New Event Trigger",
      "type": "n8n-nodes-base.googleCalendarTrigger",
      "position": [
        880,
        240
      ],
      "parameters": {
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "eventCreated",
        "calendarId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c6cfc58d-a2cc-4e28-8aec-099a1cb384b0",
      "name": "Code \u2014 Parse & Enrich Event Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1104,
        240
      ],
      "parameters": {
        "jsCode": "const ev = $input.first().json;\n\n// Parse start time \u2014 handle both dateTime and all-day (date only)\nconst startRaw = ev.start?.dateTime || ev.start?.date || '';\nconst endRaw   = ev.end?.dateTime   || ev.end?.date   || '';\n\nconst startDate = new Date(startRaw);\nconst endDate   = new Date(endRaw);\nconst now       = new Date();\n\nconst msUntil = startDate.getTime() - now.getTime();\nconst hoursUntil = msUntil / (1000 * 60 * 60);\nconst daysUntil  = Math.floor(hoursUntil / 24);\n\n// Format readable date\nconst dateOptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };\nconst timeOptions = { hour: '2-digit', minute: '2-digit', timeZoneName: 'short' };\nconst eventDateStr = startDate.toLocaleDateString('en-US', dateOptions);\nconst eventTimeStr = startDate.toLocaleTimeString('en-US', timeOptions);\n\nconst title    = ev.summary || 'Upcoming Event';\nconst desc     = (ev.description || '').replace(/<[^>]+>/g, '').trim().substring(0, 400);\nconst location = ev.location || 'Online / TBD';\nconst eventId  = ev.id || ev.iCalUID || `evt-${Date.now()}`;\nconst organizer = ev.organizer?.displayName || ev.organizer?.email || 'Organizer';\nconst eventUrl  = ev.htmlLink || '';\n\nreturn [{\n  json: {\n    eventId,\n    title,\n    description: desc,\n    location,\n    organizer,\n    eventUrl,\n    startRaw,\n    endRaw,\n    eventDateStr,\n    eventTimeStr,\n    hoursUntil: Math.round(hoursUntil),\n    daysUntil\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1a86d41e-33d1-4aee-89ea-df64537d0405",
      "name": "IF \u2014 Event Has Enough Lead Time (2h+)",
      "type": "n8n-nodes-base.if",
      "position": [
        1328,
        240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "future-check",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.hoursUntil }}",
              "rightValue": 2
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "aab53c6e-3c39-4ec5-8618-06478501eeb0",
      "name": "HTTP \u2014 Fetch Base Banner Template",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1552,
        240
      ],
      "parameters": {
        "url": "https://your-brand-assets.com/event-banner-template.jpg",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "maxRedirects": 3
            }
          },
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "048d9a2c-363c-45cd-86ea-98aeb8ef3c0a",
      "name": "Edit Image \u2014 Compose Event Promo Graphic",
      "type": "n8n-nodes-base.editImage",
      "position": [
        1760,
        240
      ],
      "parameters": {
        "options": {},
        "operation": "composite"
      },
      "typeVersion": 1
    },
    {
      "id": "68b5990f-faad-4383-b06b-c14421569e55",
      "name": "DateTime \u2014 Parse Event Start Time",
      "type": "n8n-nodes-base.dateTime",
      "position": [
        2208,
        240
      ],
      "parameters": {
        "operation": "toDate"
      },
      "typeVersion": 2
    },
    {
      "id": "1bddf0b8-6c47-4da8-9380-f4955c17d648",
      "name": "Code \u2014 Build 3 Schedule Slots",
      "type": "n8n-nodes-base.code",
      "position": [
        2432,
        240
      ],
      "parameters": {
        "jsCode": "const eventData = $('Code \u2014 Parse & Enrich Event Data').item.json;\nconst uploadResp = $('UploadToURL \u2014 Host Promo Graphic').item.json;\nconst startRaw   = eventData.startRaw;\n\nconst hostedImageUrl =\n  uploadResp?.url ??\n  uploadResp?.data?.url ??\n  uploadResp?.file?.url ??\n  uploadResp?.link ??\n  'https://placeholder.com/event-graphic.jpg';\n\nconst startMs = new Date(startRaw).getTime();\n\n// Build 3 schedule slots\nconst slots = [\n  {\n    slotIndex: 1,\n    label: '48 Hours Before',\n    urgency: 'early',\n    scheduledAt: new Date(startMs - 48 * 60 * 60 * 1000).toISOString()\n  },\n  {\n    slotIndex: 2,\n    label: '24 Hours Before',\n    urgency: 'reminder',\n    scheduledAt: new Date(startMs - 24 * 60 * 60 * 1000).toISOString()\n  },\n  {\n    slotIndex: 3,\n    label: '1 Hour Before',\n    urgency: 'last_call',\n    scheduledAt: new Date(startMs - 1 * 60 * 60 * 1000).toISOString()\n  }\n];\n\n// Return one item per schedule slot\nreturn slots.map(slot => ({\n  json: {\n    ...eventData,\n    hostedImageUrl,\n    slotIndex:   slot.slotIndex,\n    slotLabel:   slot.label,\n    urgency:     slot.urgency,\n    scheduledAt: slot.scheduledAt\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "ebe7e9cb-9ec6-4607-81af-9e5f7bb06385",
      "name": "Loop Over Items \u2014 Each Schedule Slot",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        2640,
        240
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "0beaf14c-95c3-4229-bf00-76505fcbf604",
      "name": "Wait \u2014 Until Scheduled Post Time",
      "type": "n8n-nodes-base.wait",
      "position": [
        2864,
        240
      ],
      "parameters": {
        "amount": 1
      },
      "typeVersion": 1.1
    },
    {
      "id": "6129c959-f975-4c16-a1e9-eacff1b31c22",
      "name": "OpenAI \u2014 Generate Time-Aware Captions",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        3088,
        240
      ],
      "parameters": {
        "resource": "chat"
      },
      "typeVersion": 1.4
    },
    {
      "id": "c2249c43-5c44-4241-9e79-cd0ed599c69f",
      "name": "Code \u2014 Parse Captions with Fallback",
      "type": "n8n-nodes-base.code",
      "position": [
        3312,
        240
      ],
      "parameters": {
        "jsCode": "const aiResp   = $input.first().json;\nconst slotData = $('Wait \u2014 Until Scheduled Post Time').item.json;\n\nlet captions = {};\ntry {\n  const raw     = aiResp?.choices?.[0]?.message?.content || '{}';\n  const cleaned = raw.replace(/```json|```/g, '').trim();\n  captions      = JSON.parse(cleaned);\n} catch(e) {\n  const fallback = `\ud83c\udf89 ${slotData.title} \u2014 ${slotData.eventDateStr} at ${slotData.location}. Don't miss it! ${slotData.eventUrl}`;\n  captions = {\n    linkedin: fallback + ' #Event #MustAttend',\n    twitter:  `\ud83d\udcc5 ${slotData.title} \u2014 ${slotData.eventDateStr} ${slotData.eventUrl} #Event`,\n    facebook: fallback + ' #Community #Event'\n  };\n}\n\nreturn [{\n  json: {\n    ...slotData,\n    linkedinCaption: captions.linkedin  || '',\n    twitterCaption:  captions.twitter   || '',\n    facebookCaption: captions.facebook  || '',\n    postedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cec16981-538f-4e9f-b532-438fb848ec02",
      "name": "Google Sheets \u2014 Append Post Log Row",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3632,
        -160
      ],
      "parameters": {
        "columns": {
          "value": {
            "Status": "posted",
            "EventID": "={{ $json.eventId }}",
            "ImageURL": "={{ $json.hostedImageUrl }}",
            "Platform": "LinkedIn, Twitter/X, Facebook",
            "PostedAt": "={{ $json.postedAt }}",
            "EventName": "={{ $json.title }}",
            "SlotLabel": "={{ $json.slotLabel }}",
            "ScheduledTime": "={{ $json.scheduledAt }}"
          },
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "EventPostLog"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "d35ed283-f9d9-44af-8ed2-330593cd39b3",
      "name": "LinkedIn \u2014 Publish Event Post",
      "type": "n8n-nodes-base.linkedIn",
      "position": [
        3632,
        32
      ],
      "parameters": {
        "text": "={{ $json.linkedinCaption }}",
        "additionalFields": {
          "title": "={{ $json.title }}"
        },
        "shareMediaCategory": "IMAGE"
      },
      "typeVersion": 1
    },
    {
      "id": "29d914de-5961-4cb0-84da-94499264bfe9",
      "name": "Twitter/X \u2014 Publish Event Tweet",
      "type": "n8n-nodes-base.twitter",
      "position": [
        3632,
        240
      ],
      "parameters": {
        "text": "={{ $json.twitterCaption }}",
        "additionalFields": {}
      },
      "typeVersion": 1
    },
    {
      "id": "31da4939-683c-44d7-8b53-78dd5f332eec",
      "name": "Facebook \u2014 Publish Event Photo Post",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3632,
        432
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/YOUR_FB_PAGE_ID/photos",
        "method": "POST",
        "options": {
          "timeout": 15000
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $json.hostedImageUrl }}"
            },
            {
              "name": "message",
              "value": "={{ $json.facebookCaption }}"
            },
            {
              "name": "access_token",
              "value": "=YOUR_FB_ACCESS_TOKEN"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "979f52f6-fe37-46ce-8377-0e0a03584f7a",
      "name": "Merge \u2014 Collect All Platform Results",
      "type": "n8n-nodes-base.merge",
      "position": [
        3904,
        32
      ],
      "parameters": {
        "mode": "passThrough"
      },
      "typeVersion": 3
    },
    {
      "id": "c0e9c7db-9bd3-4da5-af40-d5b075007f8d",
      "name": "Telegram \u2014 Notify Admin of Post Dispatch",
      "type": "n8n-nodes-base.telegram",
      "position": [
        4080,
        32
      ],
      "parameters": {
        "text": "=\u2705 Event promo posts dispatched!\n\n\ud83d\udcc5 *{{ $('Code \u2014 Parse Captions with Fallback').item.json.title }}*\n\ud83d\uddd3 {{ $('Code \u2014 Parse Captions with Fallback').item.json.eventDateStr }}\n\ud83d\udccd {{ $('Code \u2014 Parse Captions with Fallback').item.json.location }}\n\n\ud83d\udd16 Slot: {{ $('Code \u2014 Parse Captions with Fallback').item.json.slotLabel }}\n\ud83d\uddbc Graphic: {{ $('Code \u2014 Parse Captions with Fallback').item.json.hostedImageUrl }}\n\nPosted to: LinkedIn \u2713 | Twitter/X \u2713 | Facebook \u2713",
        "chatId": "=YOUR_TELEGRAM_CHAT_ID",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "282e46b6-5121-4563-abca-bb2a43b7511b",
      "name": "Upload a File",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        1968,
        240
      ],
      "parameters": {},
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Upload a File": {
      "main": [
        [
          {
            "node": "DateTime \u2014 Parse Event Start Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Build 3 Schedule Slots": {
      "main": [
        [
          {
            "node": "Loop Over Items \u2014 Each Schedule Slot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn \u2014 Publish Event Post": {
      "main": [
        [
          {
            "node": "Merge \u2014 Collect All Platform Results",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Code \u2014 Parse & Enrich Event Data": {
      "main": [
        [
          {
            "node": "IF \u2014 Event Has Enough Lead Time (2h+)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait \u2014 Until Scheduled Post Time": {
      "main": [
        [
          {
            "node": "OpenAI \u2014 Generate Time-Aware Captions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DateTime \u2014 Parse Event Start Time": {
      "main": [
        [
          {
            "node": "Code \u2014 Build 3 Schedule Slots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP \u2014 Fetch Base Banner Template": {
      "main": [
        [
          {
            "node": "Edit Image \u2014 Compose Event Promo Graphic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Parse Captions with Fallback": {
      "main": [
        [
          {
            "node": "Google Sheets \u2014 Append Post Log Row",
            "type": "main",
            "index": 0
          },
          {
            "node": "LinkedIn \u2014 Publish Event Post",
            "type": "main",
            "index": 0
          },
          {
            "node": "Twitter/X \u2014 Publish Event Tweet",
            "type": "main",
            "index": 0
          },
          {
            "node": "Facebook \u2014 Publish Event Photo Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Calendar \u2014 New Event Trigger": {
      "main": [
        [
          {
            "node": "Code \u2014 Parse & Enrich Event Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2014 Append Post Log Row": {
      "main": [
        [
          {
            "node": "Merge \u2014 Collect All Platform Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items \u2014 Each Schedule Slot": {
      "main": [
        [
          {
            "node": "Wait \u2014 Until Scheduled Post Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge \u2014 Collect All Platform Results": {
      "main": [
        [
          {
            "node": "Telegram \u2014 Notify Admin of Post Dispatch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF \u2014 Event Has Enough Lead Time (2h+)": {
      "main": [
        [
          {
            "node": "HTTP \u2014 Fetch Base Banner Template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI \u2014 Generate Time-Aware Captions": {
      "main": [
        [
          {
            "node": "Code \u2014 Parse Captions with Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Image \u2014 Compose Event Promo Graphic": {
      "main": [
        [
          {
            "node": "Upload a File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}