{
  "nodes": [
    {
      "id": "cd37d46a-f769-458d-b7f5-7019f777ea06",
      "name": "Campaign Upload Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1344,
        96
      ],
      "parameters": {
        "path": "campaign-upload",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "lastNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "9fc5e2b2-6dab-4abe-bd8c-a62462f5a72e",
      "name": "Workflow Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -1168,
        96
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "maxEmailsPerInboxPerHour",
              "type": "number",
              "value": 50
            },
            {
              "id": "id-2",
              "name": "maxEmailsPerInboxPerDay",
              "type": "number",
              "value": 500
            },
            {
              "id": "id-3",
              "name": "minDelayBetweenEmailsMinutes",
              "type": "number",
              "value": 2
            },
            {
              "id": "id-4",
              "name": "mxRecordCheckApiUrl",
              "type": "string",
              "value": "https://dns.google/resolve"
            },
            {
              "id": "id-5",
              "name": "defaultTimezone",
              "type": "string",
              "value": "UTC"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "3952be89-1a72-4ec4-832e-af07b5116b2c",
      "name": "Extract CSV Leads",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -896,
        96
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.1
    },
    {
      "id": "5808ce88-41d3-421b-a8ca-64345c6cdcd5",
      "name": "Split Leads",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -672,
        96
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "data"
      },
      "typeVersion": 1
    },
    {
      "id": "1ac2e8a4-ca32-4fe6-b0ae-3e31787a4d11",
      "name": "Validate Email & Extract Domain",
      "type": "n8n-nodes-base.code",
      "position": [
        -496,
        96
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Validate email syntax and extract domain information\nconst email = $input.item.json.email;\n\n// Email validation regex (RFC 5322 simplified)\nconst emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;\n\nconst isValid = emailRegex.test(email);\n\nlet domain = null;\nif (isValid && email) {\n  const parts = email.split('@');\n  if (parts.length === 2) {\n    domain = parts[1].toLowerCase();\n  }\n}\n\n// Return the original data plus validation results\nreturn {\n  ...($input.item.json),\n  isValid: isValid,\n  domain: domain,\n  validatedAt: new Date().toISOString()\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c1868399-326e-4b25-8885-7e0361716bd3",
      "name": "Is Email Valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        -224,
        96
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $('Validate Email & Extract Domain').item.json.isValid }}"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "8f9fff06-c9ec-4231-b7fc-9fe49848da31",
      "name": "Check Domain MX Records",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        96,
        0
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.mxRecordCheckApiUrl }}?name={{ $json.domain }}&type=MX",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "0cd57923-f1a2-48ec-af11-1a9ab4bfa27c",
      "name": "Enrich Lead Data",
      "type": "n8n-nodes-base.code",
      "position": [
        448,
        0
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Extract timezone from email domain, determine domain type, calculate engagement score\nconst item = $input.item.json;\n\n// Extract domain from email\nconst email = item.email || '';\nconst domain = email.split('@')[1] || '';\n\n// Determine domain type\nlet domainType = 'corporate';\nconst freeEmailProviders = ['gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'aol.com', 'icloud.com', 'protonmail.com', 'mail.com'];\n\nif (freeEmailProviders.includes(domain.toLowerCase())) {\n  if (domain.toLowerCase().includes('gmail')) {\n    domainType = 'gmail';\n  } else if (domain.toLowerCase().includes('outlook') || domain.toLowerCase().includes('hotmail')) {\n    domainType = 'outlook';\n  } else {\n    domainType = 'free';\n  }\n}\n\n// Extract timezone (simplified - based on domain TLD or default to UTC)\n// In production, you'd use a GeoIP service or domain lookup\nlet timezone = 'UTC';\nconst tld = domain.split('.').pop();\n\n// Simple TLD to timezone mapping\nconst tldTimezones = {\n  'us': 'America/New_York',\n  'uk': 'Europe/London',\n  'ca': 'America/Toronto',\n  'au': 'Australia/Sydney',\n  'de': 'Europe/Berlin',\n  'fr': 'Europe/Paris',\n  'jp': 'Asia/Tokyo',\n  'in': 'Asia/Kolkata',\n  'br': 'America/Sao_Paulo'\n};\n\ntimezone = tldTimezones[tld] || 'UTC';\n\n// Calculate engagement score (0-100)\n// Factors: domain type, MX records validity, domain age estimate\nlet engagementScore = 50; // Base score\n\n// Domain type scoring\nif (domainType === 'corporate') {\n  engagementScore += 20; // Corporate emails tend to have higher engagement\n} else if (domainType === 'gmail') {\n  engagementScore += 10;\n} else if (domainType === 'outlook') {\n  engagementScore += 5;\n}\n\n// MX records validity (if available from previous node)\nif (item.mxValid === true || item.hasMxRecords === true) {\n  engagementScore += 15;\n}\n\n// Domain length (shorter corporate domains often indicate established companies)\nif (domainType === 'corporate' && domain.length < 15) {\n  engagementScore += 10;\n}\n\n// Ensure score is within 0-100 range\nengagementScore = Math.min(100, Math.max(0, engagementScore));\n\n// Merge with original lead data\nconst enrichedLead = {\n  ...item,\n  domain: domain,\n  domainType: domainType,\n  timezone: timezone,\n  engagementScore: engagementScore,\n  enrichedAt: new Date().toISOString(),\n  isEnriched: true\n};\n\nreturn enrichedLead;"
      },
      "typeVersion": 2
    },
    {
      "id": "30771931-e4b9-446c-8fd4-9168fb388209",
      "name": "Store Valid Lead",
      "type": "n8n-nodes-base.postgres",
      "position": [
        672,
        0
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "leads"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "email": "={{ $json.email }}",
            "domain": "={{ $json.domain }}",
            "status": "pending",
            "timezone": "={{ $json.timezone }}",
            "campaign_id": "={{ $json.campaign_id }}",
            "domain_type": "={{ $json.domain_type }}",
            "engagement_score": "={{ $json.engagement_score }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {}
      },
      "typeVersion": 2.6
    },
    {
      "id": "be00b299-c935-4b8a-a08d-b3d6e09eacef",
      "name": "Store Invalid Lead",
      "type": "n8n-nodes-base.postgres",
      "position": [
        96,
        208
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "invalid_leads"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "email": "={{ $json.email }}",
            "reason": "invalid_syntax_or_domain",
            "campaign_id": "={{ $json.campaign_id }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {}
      },
      "typeVersion": 2.6
    },
    {
      "id": "5efbf8aa-2368-452d-911c-2fc5504be140",
      "name": "Campaign Scheduler",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1344,
        688
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "f431a43a-4712-4436-9c43-e31f6cfdd3dc",
      "name": "Get Pending Campaigns",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -1152,
        688
      ],
      "parameters": {
        "query": "SELECT * FROM campaigns WHERE status = 'active' AND scheduled_time <= NOW()",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "1913e611-eae0-4555-9750-3b33b86413d6",
      "name": "Split Campaigns",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -896,
        688
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "data"
      },
      "typeVersion": 1
    },
    {
      "id": "3b96f62b-f494-45ed-a286-f6f520bf9413",
      "name": "Get Campaign Leads",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -672,
        688
      ],
      "parameters": {
        "query": "SELECT l.*, st.best_send_hour FROM leads l LEFT JOIN send_time_rules st ON l.timezone = st.timezone WHERE l.campaign_id = {{ $json.id }} AND l.status = 'pending' ORDER BY l.engagement_score DESC LIMIT 100",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "bc92f362-f8a6-4372-ae89-2860bd776c9c",
      "name": "Split Campaign Leads",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -464,
        688
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "data"
      },
      "typeVersion": 1
    },
    {
      "id": "7a980288-ec94-4bd1-8e58-4c2b0134e1a1",
      "name": "Calculate Send Time & Select Inbox",
      "type": "n8n-nodes-base.code",
      "position": [
        -288,
        688
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Calculate optimal send time and select inbox with round-robin and throttle limits\n\nconst item = $input.item.json;\n\n// Extract lead and campaign data\nconst leadTimezone = item.timezone || 'America/New_York';\nconst bestSendHour = item.best_send_hour || 10; // Default to 10 AM\nconst campaignId = item.campaign_id;\n\n// Calculate optimal send time based on timezone and best send hour\nconst now = new Date();\nconst sendTime = new Date();\n\n// Convert to lead's timezone and set to best send hour\ntry {\n  const luxon = require('luxon');\n  const dt = luxon.DateTime.now().setZone(leadTimezone);\n  \n  // Set to best send hour today\n  let targetTime = dt.set({ hour: bestSendHour, minute: 0, second: 0, millisecond: 0 });\n  \n  // If the time has passed today, schedule for tomorrow\n  if (targetTime < dt) {\n    targetTime = targetTime.plus({ days: 1 });\n  }\n  \n  sendTime.setTime(targetTime.toMillis());\n} catch (error) {\n  // Fallback: schedule for next occurrence of best send hour\n  sendTime.setHours(bestSendHour, 0, 0, 0);\n  if (sendTime <= now) {\n    sendTime.setDate(sendTime.getDate() + 1);\n  }\n}\n\n// Get available inboxes (this would typically come from a database query)\n// For now, we'll use a placeholder that should be replaced with actual inbox data\nconst inboxes = item.available_inboxes || [];\n\nif (!inboxes || inboxes.length === 0) {\n  throw new Error('No available inboxes found for campaign');\n}\n\n// Round-robin inbox selection with throttle limit checking\nlet selectedInbox = null;\nconst currentHour = now.getHours();\nconst currentDate = now.toISOString().split('T')[0];\n\nfor (let i = 0; i < inboxes.length; i++) {\n  const inbox = inboxes[i];\n  \n  // Check daily limit\n  const dailySent = inbox.daily_sent || 0;\n  const dailyLimit = inbox.daily_limit || 500;\n  \n  // Check hourly limit\n  const hourlySent = inbox.hourly_sent || 0;\n  const hourlyLimit = inbox.hourly_limit || 50;\n  \n  // Check if inbox is within limits\n  if (dailySent < dailyLimit && hourlySent < hourlyLimit) {\n    selectedInbox = inbox;\n    break;\n  }\n}\n\nif (!selectedInbox) {\n  throw new Error('All inboxes have reached their throttle limits');\n}\n\n// Return the calculated data\nreturn {\n  ...item,\n  sendTime: sendTime.toISOString(),\n  selectedInbox: {\n    id: selectedInbox.id,\n    email: selectedInbox.email,\n    daily_sent: selectedInbox.daily_sent || 0,\n    daily_limit: selectedInbox.daily_limit || 500,\n    hourly_sent: selectedInbox.hourly_sent || 0,\n    hourly_limit: selectedInbox.hourly_limit || 50\n  },\n  calculatedAt: now.toISOString()\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c2ed8ee4-96e1-4867-961a-31c1f22d2310",
      "name": "Wait Until Send Time",
      "type": "n8n-nodes-base.wait",
      "position": [
        0,
        688
      ],
      "parameters": {
        "resume": "specificTime",
        "dateTime": "={{ $json.sendTime }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "40b97f6f-7e1a-45c3-8fb7-9435014d0d76",
      "name": "Send Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        224,
        688
      ],
      "parameters": {
        "sendTo": "={{ $json.email }}",
        "message": "={{ $json.emailBody }}",
        "options": {
          "senderName": "={{ $json.selectedInbox }}"
        },
        "subject": "={{ $json.subject }}"
      },
      "typeVersion": 2.2
    },
    {
      "id": "c3e86c12-f430-4149-8e8d-ed7cc09f6c08",
      "name": "Log Send Event",
      "type": "n8n-nodes-base.postgres",
      "position": [
        464,
        688
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "email_events"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "lead_id": "={{ $json.lead_id }}",
            "sent_at": "={{ $now.toISO() }}",
            "event_type": "sent",
            "inbox_used": "={{ $json.inbox_used }}",
            "campaign_id": "={{ $json.campaign_id }}"
          },
          "schema": [
            {
              "id": "lead_id",
              "required": false,
              "displayName": "lead_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "campaign_id",
              "required": false,
              "displayName": "campaign_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "event_type",
              "required": false,
              "displayName": "event_type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "inbox_used",
              "required": false,
              "displayName": "inbox_used",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sent_at",
              "required": false,
              "displayName": "sent_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {}
      },
      "typeVersion": 2.6
    },
    {
      "id": "6abae748-9768-426a-ad65-38dff04ffb12",
      "name": "Update Inbox Stats",
      "type": "n8n-nodes-base.postgres",
      "position": [
        688,
        688
      ],
      "parameters": {
        "query": "UPDATE inbox_stats SET emails_sent_today = emails_sent_today + 1, emails_sent_this_hour = emails_sent_this_hour + 1, last_send_time = NOW() WHERE inbox_email = '={{ $json.selectedInbox }}'",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "48e31d93-c6bd-4905-aa79-fac9540f1cf8",
      "name": "Email Event Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1392,
        1040
      ],
      "parameters": {
        "path": "email-events",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "1901961d-dc62-4ef3-ab2d-03b773f0bf73",
      "name": "Parse Event Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -1088,
        1040
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Parse webhook payload for email events\nconst payload = $input.item.json;\n\n// Extract event data from webhook payload\nconst eventType = payload.event_type || payload.eventType || payload.type || 'unknown';\nconst emailIdentifier = payload.email_id || payload.emailId || payload.message_id || payload.messageId || '';\nconst timestamp = payload.timestamp || payload.event_time || payload.eventTime || new Date().toISOString();\n\n// Extract additional event details\nconst recipientEmail = payload.recipient || payload.to || payload.email || '';\nconst campaignId = payload.campaign_id || payload.campaignId || '';\nconst leadId = payload.lead_id || payload.leadId || '';\n\n// Extract event-specific data\nlet eventDetails = {};\n\nswitch(eventType.toLowerCase()) {\n  case 'open':\n    eventDetails = {\n      userAgent: payload.user_agent || payload.userAgent || '',\n      ipAddress: payload.ip_address || payload.ipAddress || '',\n      location: payload.location || ''\n    };\n    break;\n  \n  case 'click':\n    eventDetails = {\n      clickedUrl: payload.url || payload.link || '',\n      userAgent: payload.user_agent || payload.userAgent || '',\n      ipAddress: payload.ip_address || payload.ipAddress || ''\n    };\n    break;\n  \n  case 'reply':\n    eventDetails = {\n      replyContent: payload.reply_content || payload.content || '',\n      replySubject: payload.subject || ''\n    };\n    break;\n  \n  case 'bounce':\n    eventDetails = {\n      bounceType: payload.bounce_type || payload.bounceType || 'hard',\n      bounceReason: payload.reason || payload.bounce_reason || '',\n      diagnosticCode: payload.diagnostic_code || ''\n    };\n    break;\n  \n  default:\n    eventDetails = payload;\n}\n\n// Return structured event data\nreturn {\n  eventType: eventType.toLowerCase(),\n  emailIdentifier,\n  timestamp,\n  recipientEmail,\n  campaignId,\n  leadId,\n  eventDetails,\n  rawPayload: payload\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "3266e902-d2ca-4edb-bc64-8cc7a985b9ba",
      "name": "Log Email Event",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -800,
        1040
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "email_events"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "lead_id": "={{ $json.lead_id }}",
            "event_type": "={{ $json.event_type }}",
            "inbox_used": "={{ $json.inbox_used }}",
            "campaign_id": "={{ $json.campaign_id }}",
            "event_timestamp": "={{ $json.event_timestamp }}"
          },
          "schema": [
            {
              "id": "lead_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "lead_id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "campaign_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "campaign_id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "event_type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "event_type",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "event_timestamp",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "event_timestamp",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "inbox_used",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "inbox_used",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "lead_id",
            "campaign_id",
            "event_type",
            "event_timestamp",
            "inbox_used"
          ]
        },
        "options": {}
      },
      "typeVersion": 2.6
    },
    {
      "id": "2854d08a-fb91-4fe8-9ab6-a1ececaadc8d",
      "name": "Update Performance Stats",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -528,
        1040
      ],
      "parameters": {
        "query": "UPDATE campaign_stats SET total_opens = total_opens + CASE WHEN '{{ $json.event_type }}' = 'open' THEN 1 ELSE 0 END, total_clicks = total_clicks + CASE WHEN '{{ $json.event_type }}' = 'click' THEN 1 ELSE 0 END, total_replies = total_replies + CASE WHEN '{{ $json.event_type }}' = 'reply' THEN 1 ELSE 0 END WHERE campaign_id = {{ $json.campaign_id }}",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "fef6b1e5-9d82-4cc0-9b01-42abcc49ef88",
      "name": "Analytics Scheduler",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1408,
        1392
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "48403e27-7aa8-4f3d-987f-8d317489d3e9",
      "name": "Get Performance Data",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -1024,
        1392
      ],
      "parameters": {
        "query": "SELECT EXTRACT(HOUR FROM event_timestamp) as hour, l.timezone, COUNT(*) as open_count FROM email_events e JOIN leads l ON e.lead_id = l.id WHERE e.event_type = 'open' AND e.event_timestamp > NOW() - INTERVAL '30 days' GROUP BY hour, l.timezone ORDER BY l.timezone, open_count DESC",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "25dd805e-0568-473e-b84d-2c2b3b071fa9",
      "name": "Calculate Best Send Times",
      "type": "n8n-nodes-base.code",
      "position": [
        -800,
        1392
      ],
      "parameters": {
        "jsCode": "// Analyze performance data to find optimal send times by timezone\nconst items = $input.all();\n\n// Group data by timezone and hour\nconst timezoneHourStats = {};\n\nfor (const item of items) {\n  const timezone = item.json.timezone || 'UTC';\n  const sendHour = item.json.send_hour;\n  const openRate = parseFloat(item.json.open_rate) || 0;\n  const totalSent = parseInt(item.json.total_sent) || 0;\n  const totalOpened = parseInt(item.json.total_opened) || 0;\n  \n  if (!timezoneHourStats[timezone]) {\n    timezoneHourStats[timezone] = {};\n  }\n  \n  if (!timezoneHourStats[timezone][sendHour]) {\n    timezoneHourStats[timezone][sendHour] = {\n      totalSent: 0,\n      totalOpened: 0,\n      openRate: 0,\n      count: 0\n    };\n  }\n  \n  timezoneHourStats[timezone][sendHour].totalSent += totalSent;\n  timezoneHourStats[timezone][sendHour].totalOpened += totalOpened;\n  timezoneHourStats[timezone][sendHour].count += 1;\n}\n\n// Calculate average open rates and find best send times\nconst results = [];\n\nfor (const timezone in timezoneHourStats) {\n  let bestHour = null;\n  let bestOpenRate = 0;\n  \n  for (const hour in timezoneHourStats[timezone]) {\n    const stats = timezoneHourStats[timezone][hour];\n    \n    // Calculate weighted open rate\n    if (stats.totalSent > 0) {\n      stats.openRate = (stats.totalOpened / stats.totalSent) * 100;\n      \n      // Track best performing hour\n      if (stats.openRate > bestOpenRate) {\n        bestOpenRate = stats.openRate;\n        bestHour = parseInt(hour);\n      }\n    }\n  }\n  \n  // Create result for this timezone\n  if (bestHour !== null) {\n    results.push({\n      json: {\n        timezone: timezone,\n        optimal_send_hour: bestHour,\n        best_open_rate: bestOpenRate.toFixed(2),\n        hour_stats: timezoneHourStats[timezone],\n        analyzed_at: new Date().toISOString()\n      }\n    });\n  }\n}\n\n// Sort by timezone for consistent output\nresults.sort((a, b) => a.json.timezone.localeCompare(b.json.timezone));\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "925458ce-cb95-47d4-ab87-8ba2589088a0",
      "name": "Update Send Time Rules",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -496,
        1392
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "send_time_rules"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "timezone": "={{ $json.timezone }}",
            "last_updated": "={{ $now.toISO() }}",
            "best_send_hour": "={{ $json.best_send_hour }}"
          },
          "schema": [
            {
              "id": "timezone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "timezone",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "best_send_hour",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "best_send_hour",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "last_updated",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "last_updated",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "timezone"
          ]
        },
        "options": {},
        "operation": "upsert"
      },
      "typeVersion": 2.6
    },
    {
      "id": "d362c82d-f245-403e-9f97-01fa0c52c328",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2528,
        272
      ],
      "parameters": {
        "width": 448,
        "height": 560,
        "content": "\n## How it works\nThis workflow automates end-to-end email campaigns with built-in deliverability protection and smart optimization. It ingests campaign data and CSV leads, validates emails, checks domains via MX records, and enriches leads with timezone and engagement scores.\n\nCampaigns are executed using a scheduler that selects top leads and calculates optimal send times based on timezone and past performance. Emails are sent through rotating inboxes with strict limits, while all events are tracked to continuously improve performance.\n\n## Setup\n\n- Connect webhook for campaign and CSV upload  \n- Configure send limits, delays, and MX API  \n- Set up Postgres with required tables  \n- Connect Gmail or SMTP for sending  \n- Configure event webhook for tracking  \n- Enable campaign and analytics schedulers  \n- Test with sample campaign before activating  \n"
      },
      "typeVersion": 1
    },
    {
      "id": "21b3c85d-68a7-407a-899f-309f08d14f5f",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1424,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 432,
        "content": "## Campaign Setup\nReceives campaign data and CSV leads. Configures limits, delays, and validation settings."
      },
      "typeVersion": 1
    },
    {
      "id": "eb10cf7a-9949-4308-ba8f-4f0ac24fcc5f",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -944,
        -48
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 352,
        "content": "## Lead Extraction\nParses uploaded CSV and splits leads into individual records for processing."
      },
      "typeVersion": 1
    },
    {
      "id": "bcf907e7-b88a-4786-928b-130239652069",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -544,
        -48
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 352,
        "content": "## Lead Extraction\nParses uploaded CSV and splits leads into individual records for processing."
      },
      "typeVersion": 1
    },
    {
      "id": "aa32dd2f-5e13-45a6-8ed4-4c15a5658c75",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -256,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 416,
        "content": "## Validation Routing\nValid leads continue to processing. Invalid ones are stored separately."
      },
      "typeVersion": 1
    },
    {
      "id": "6d0d02fc-f7df-4675-b684-6baed29b6dde",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 352,
        "content": "## Lead Enrichment\nVerifies domain via MX records, assigns timezone, and calculates engagement score before storing."
      },
      "typeVersion": 1
    },
    {
      "id": "92a3d87a-763c-4f05-b445-a6c5fac489e1",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        416,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 352,
        "content": "## Send Tracking\nLogs sent emails and updates inbox usage statistics."
      },
      "typeVersion": 1
    },
    {
      "id": "bd5ca422-1ff4-42d6-b015-a893e8928097",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1152,
        1280
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 304,
        "content": "## Performance Analysis\nAnalyzes historical data to identify best-performing send times."
      },
      "typeVersion": 1
    },
    {
      "id": "0ff46a9d-5be8-4a98-b27d-2c16b1e96056",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -608,
        1264
      ],
      "parameters": {
        "color": 7,
        "width": 272,
        "height": 304,
        "content": "## Continuous Optimization\nUpdates send-time rules to improve future campaign performance."
      },
      "typeVersion": 1
    },
    {
      "id": "7cc7a2d5-c41f-461e-a27f-133e057e1f92",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        928
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 304,
        "content": "## Performance Update\nUpdates campaign performance metrics based on user interactions."
      },
      "typeVersion": 1
    },
    {
      "id": "97438aba-43b1-4203-8509-40e5a2555922",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        560
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 272,
        "content": "## Email Delivery\nWaits until optimal time and sends emails using selected inbox."
      },
      "typeVersion": 1
    },
    {
      "id": "e6618eba-0b36-4a9e-a02d-1ffe6085375d",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -912,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 304,
        "content": "## Lead Selection\nPulls top leads sorted by engagement score and readiness."
      },
      "typeVersion": 1
    },
    {
      "id": "f89bd8ae-8836-4930-9401-58515cb0569e",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1200,
        944
      ],
      "parameters": {
        "color": 7,
        "width": 304,
        "height": 272,
        "content": "## Event Processing\nParses event data and logs it for tracking and analytics."
      },
      "typeVersion": 1
    },
    {
      "id": "825e50a6-af56-48ce-99d4-a70acadb566d",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1472,
        592
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 288,
        "content": "## Campaign Execution\nFetches active campaigns and prepares them for email sending."
      },
      "typeVersion": 1
    },
    {
      "id": "d8d560eb-e0e8-4a76-a982-0d150fda641d",
      "name": "Sticky Note15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -496,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 304,
        "content": "## Send Optimization\nCalculates best send time per lead and selects inbox using rotation and limits."
      },
      "typeVersion": 1
    },
    {
      "id": "c982d118-1ec4-4b85-b2a1-bdc0a80a985c",
      "name": "Sticky Note16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1504,
        944
      ],
      "parameters": {
        "color": 7,
        "width": 272,
        "height": 272,
        "content": "## Event Intake\nReceives email events like opens, clicks, replies, and bounces."
      },
      "typeVersion": 1
    },
    {
      "id": "1f2fef7b-bb6c-45b9-b697-eb99635d4c84",
      "name": "Sticky Note17",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1520,
        1280
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 304,
        "content": "## Analytics Trigger\nRuns periodically to analyze campaign performance."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Send Email": {
      "main": [
        [
          {
            "node": "Log Send Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Leads": {
      "main": [
        [
          {
            "node": "Validate Email & Extract Domain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Send Event": {
      "main": [
        [
          {
            "node": "Update Inbox Stats",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Email Valid?": {
      "main": [
        [
          {
            "node": "Check Domain MX Records",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Store Invalid Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Email Event": {
      "main": [
        [
          {
            "node": "Update Performance Stats",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Campaigns": {
      "main": [
        [
          {
            "node": "Get Campaign Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enrich Lead Data": {
      "main": [
        [
          {
            "node": "Store Valid Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Event Data": {
      "main": [
        [
          {
            "node": "Log Email Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract CSV Leads": {
      "main": [
        [
          {
            "node": "Split Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Campaign Scheduler": {
      "main": [
        [
          {
            "node": "Get Pending Campaigns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Campaign Leads": {
      "main": [
        [
          {
            "node": "Split Campaign Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analytics Scheduler": {
      "main": [
        [
          {
            "node": "Get Performance Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Event Webhook": {
      "main": [
        [
          {
            "node": "Parse Event Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Performance Data": {
      "main": [
        [
          {
            "node": "Calculate Best Send Times",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Campaign Leads": {
      "main": [
        [
          {
            "node": "Calculate Send Time & Select Inbox",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait Until Send Time": {
      "main": [
        [
          {
            "node": "Send Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Pending Campaigns": {
      "main": [
        [
          {
            "node": "Split Campaigns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Configuration": {
      "main": [
        [
          {
            "node": "Extract CSV Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Campaign Upload Webhook": {
      "main": [
        [
          {
            "node": "Workflow Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Domain MX Records": {
      "main": [
        [
          {
            "node": "Enrich Lead Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Best Send Times": {
      "main": [
        [
          {
            "node": "Update Send Time Rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Email & Extract Domain": {
      "main": [
        [
          {
            "node": "Is Email Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Send Time & Select Inbox": {
      "main": [
        [
          {
            "node": "Wait Until Send Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}