AutomationFlows › Data & Sheets › Ai-powered Autonomous A/b Email Testing Orchestrator

Ai-powered Autonomous A/b Email Testing Orchestrator

ByRahul Joshi @rahul08✓ on n8n.io

Automate your entire email experimentation workflow with this AI-powered A/B testing orchestrator 🚀. This n8n automation generates two intelligent email variants using AI, sends campaigns through Mailjet, continuously tracks engagement performance, and automatically determines…

Event trigger★★★★★ complexity62 nodesHTTP RequestGoogle Sheets TriggerNotion
Data & Sheets Trigger: Event Nodes: 62 Complexity: ★★★★★ Added:

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

This workflow follows the Googlesheetstrigger → 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": "uwv0j8l2ATzvdHpg",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Autonomous A/B Test Orchestrator",
  "tags": [],
  "nodes": [
    {
      "id": "36414041-56c4-48bc-9a4d-de10903df58e",
      "name": "Gemini \u2014 Generate Variants1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -432,
        3648
      ],
      "parameters": {
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=YOUR_TOKEN_HERE",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"contents\": [\n    {\n      \"parts\": [\n        {\n          \"text\": \"You are a world-class email copywriter specializing in A/B testing. Generate exactly 2 email variants for testing.\\n\\nHypothesis: {{ $json.hypothesis }}\\nMetric to optimize: {{ $json.metric }}\\nEmail Subject Base: {{ $json.emailSubject }}\\n\\nReturn ONLY valid JSON (no markdown, no explanation) in this exact format:\\n{\\n  \\\"variant_a\\\": {\\n    \\\"subject\\\": \\\"subject line for variant A\\\",\\n    \\\"preview_text\\\": \\\"preview text for variant A\\\",\\n    \\\"body_html\\\": \\\"<p>Full HTML email body for variant A.</p>\\\",\\n    \\\"rationale\\\": \\\"why this variant tests the hypothesis\\\"\\n  },\\n  \\\"variant_b\\\": {\\n    \\\"subject\\\": \\\"subject line for variant B\\\",\\n    \\\"preview_text\\\": \\\"preview text for variant B\\\",\\n    \\\"body_html\\\": \\\"<p>Full HTML email body for variant B.</p>\\\",\\n    \\\"rationale\\\": \\\"why this variant contrasts with A\\\"\\n  }\\n}\"\n        }\n      ]\n    }\n  ],\n  \"generationConfig\": {\n    \"temperature\": 0.7,\n    \"maxOutputTokens\": 3000,\n    \"thinkingConfig\": {\n      \"thinkingBudget\": 0\n    }\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "a8d3a0f5-37ee-407c-b0d2-4e7efbc3023c",
      "name": "Parse Variants from Gemini1",
      "type": "n8n-nodes-base.code",
      "position": [
        -208,
        3648
      ],
      "parameters": {
        "jsCode": "// Parse Gemini's response structure\nconst geminiResponse = $input.first().json;\nconst textContent = geminiResponse.candidates?.[0]?.content?.parts?.[0]?.text;\n\nif (!textContent) throw new Error('No response from Gemini');\n\nlet variants;\ntry {\n  variants = JSON.parse(textContent);\n} catch(e) {\n  const match = textContent.match(/\\{[\\s\\S]*\\}/);\n  if (match) {\n    variants = JSON.parse(match[0]);\n  } else {\n    throw new Error('Could not parse variants JSON from Gemini: ' + textContent);\n  }\n}\n\nconst prevData = $('Parse & Validate Form Data').first().json;\n\nreturn [{\n  json: {\n    ...prevData,\n    variant_a: variants.variant_a,\n    variant_b: variants.variant_b,\n    status: 'variants_generated'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "074c52d5-f073-4764-a5be-44b328820de6",
      "name": "Prepare Mailjet Payloads1",
      "type": "n8n-nodes-base.code",
      "position": [
        32,
        3648
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\n\nconst cleanHtml = (html) => html\n  .replace(/[\\r\\n\\t]+/g, ' ')\n  .trim();\n\nreturn [{\n  json: {\n    ...data,\n    mailjet_payload_a: {\n      FromEmail: data.fromEmail,\n      FromName: data.fromName,\n      Subject: data.variant_a.subject,\n      \"Html-part\": cleanHtml(data.variant_a.body_html),\n      Recipients: [{ Email: data.fromEmail }],\n      Headers: {\n        \"Reply-To\": data.fromEmail\n      }\n    },\n    mailjet_payload_b: {\n      FromEmail: data.fromEmail,\n      FromName: data.fromName,\n      Subject: data.variant_b.subject,\n      \"Html-part\": cleanHtml(data.variant_b.body_html),\n      Recipients: [{ Email: data.fromEmail }],\n      Headers: {\n        \"Reply-To\": data.fromEmail\n      }\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "56344fe3-ca1f-4cd1-ae5e-70b3b30df4b4",
      "name": "Mailjet \u2014 Send Variant A1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        272,
        3536
      ],
      "parameters": {
        "url": "https://api.mailjet.com/v3/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.mailjet_payload_a) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Basic ODYyNDhiMjY4ZWE5ZWQ3YzFhYjU0Yjc2MGIxYjc5OTA6MmIxNmNhNzU2NWViYmI2ZDU4NDNhYzFhMjhmNTIyYmM="
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "82316cb0-d258-499c-9736-fca3ba3fa772",
      "name": "Mailjet \u2014 Send Variant B1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        272,
        3776
      ],
      "parameters": {
        "url": "https://api.mailjet.com/v3/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.mailjet_payload_b) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Basic ODYyNDhiMjY4ZWE5ZWQ3YzFhYjU0Yjc2MGIxYjc5OTA6MmIxNmNhNzU2NWViYmI2ZDU4NDNhYzFhMjhmNTIyYmM="
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "d11a7574-04ec-42f9-9533-80b7782d098f",
      "name": "Mailjet \u2014 Poll Results Variant A1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -288,
        4192
      ],
      "parameters": {
        "url": "=https://api.mailjet.com/v3/REST/message?MessageUUID={{ $json.campaignIdA }}",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Basic ODYyNDhiMjY4ZWE5ZWQ3YzFhYjU0Yjc2MGIxYjc5OTA6MmIxNmNhNzU2NWViYmI2ZDU4NDNhYzFhMjhmNTIyYmM="
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "6e186f49-e14f-41b9-abe5-610c95be0c40",
      "name": "Mailjet \u2014 Poll Results Variant B1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -288,
        4352
      ],
      "parameters": {
        "url": "=https://api.mailjet.com/v3/REST/message?MessageUUID={{ $json.campaignIdB }}",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Basic ODYyNDhiMjY4ZWE5ZWQ3YzFhYjU0Yjc2MGIxYjc5OTA6MmIxNmNhNzU2NWViYmI2ZDU4NDNhYzFhMjhmNTIyYmM="
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "235fa117-7411-41aa-bfa9-9b416e4719b7",
      "name": "Log Loser (Mailjet)1",
      "type": "n8n-nodes-base.code",
      "position": [
        1184,
        4144
      ],
      "parameters": {
        "jsCode": "// Mailjet doesn't have an archive API \u2014 log the loser UUID for reference\nconst data = $input.first().json;\nconsole.log('Loser campaign UUID:', data.loserCampaignId);\nconsole.log('Winner campaign UUID:', data.winnerCampaignId);\nreturn [{ json: { ...data, archivedAt: new Date().toISOString() } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "a43b195a-e791-41be-9df8-558b56bbe38c",
      "name": "Google Sheets Trigger",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "position": [
        -880,
        3648
      ],
      "parameters": {
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1pKhSTA4_KQfiEA69LNoY5voPvpN7vaCaYtHIVzv2EI0/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1pKhSTA4_KQfiEA69LNoY5voPvpN7vaCaYtHIVzv2EI0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1pKhSTA4_KQfiEA69LNoY5voPvpN7vaCaYtHIVzv2EI0/edit?usp=drivesdk",
          "cachedResultName": "Email responses"
        }
      },
      "credentials": {
        "googleSheetsTriggerOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "a84de7c0-16c4-4f86-800c-d9ef44f3f4f9",
      "name": "Parse & Validate Form Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -656,
        3648
      ],
      "parameters": {
        "jsCode": "// Extract and validate incoming form data\nconst body = $input.first().json.body || $input.first().json;\n\nconst hypothesis = body.hypothesis || body.field_1 || '';\nconst metric = body.metric || body.field_2 || 'open_rate';\nconst emailSubject = body.email_subject || body.field_3 || 'Test Email';\nconst audienceSize = body.audience_size || 200;\nconst testDurationDays = body.test_duration_days || 7;\nconst fromEmail = body.from_email || 'user@example.com';\nconst fromName = body.from_name || 'Techdome';\nconst listId = body.list_id || 4;\n\nif (!hypothesis) throw new Error('Missing hypothesis in form data');\nif (!metric) throw new Error('Missing metric in form data');\n\nconst testId = 'ab_' + Date.now();\nconst startDate = new Date().toISOString();\nconst endDate = new Date(Date.now() + testDurationDays * 86400000).toISOString();\n\nreturn [{\n  json: {\n    testId,\n    hypothesis,\n    metric,\n    emailSubject,\n    audienceSize,\n    testDurationDays,\n    fromEmail,\n    fromName,\n    listId,\n    startDate,\n    endDate,\n    status: 'generating_variants'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f0fe3e6f-0b70-4dcd-8ae3-db7567ea27e0",
      "name": "Merge Campaign IDs",
      "type": "n8n-nodes-base.code",
      "position": [
        800,
        3648
      ],
      "parameters": {
        "jsCode": "// Merge MessageIDs from both Mailjet sends\nconst variantAResponse = $('Mailjet \u2014 Send Variant A1').first().json;\nconst variantBResponse = $('Mailjet \u2014 Send Variant B1').first().json;\nconst parsedData = $('Prepare Mailjet Payloads1').first().json;\n\nconst campaignIdA = variantAResponse.Sent?.[0]?.MessageUUID;\nconst campaignIdB = variantBResponse.Sent?.[0]?.MessageUUID;\nconst messageIdA = variantAResponse.Sent?.[0]?.MessageID;\nconst messageIdB = variantBResponse.Sent?.[0]?.MessageID;\n\nif (!campaignIdA || !campaignIdB) {\n  throw new Error('Failed to send Mailjet emails. Response A: ' + JSON.stringify(variantAResponse) + ', Response B: ' + JSON.stringify(variantBResponse));\n}\n\nreturn [{\n  json: {\n    ...parsedData,\n    campaignIdA,\n    campaignIdB,\n    messageIdA,\n    messageIdB,\n    status: 'campaigns_sent'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "96b752e3-ed31-42e7-92e6-ba5e2d4a4aa8",
      "name": "Notion \u2014 Log Test Start",
      "type": "n8n-nodes-base.notion",
      "position": [
        1056,
        3648
      ],
      "parameters": {
        "title": "=A/B Test: {{ $json.testId }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "35ff839f-1184-804c-9b57-c474dc64d6b5",
          "cachedResultUrl": "https://www.notion.so/35ff839f1184804c9b57c474dc64d6b5",
          "cachedResultName": "Email Tracker"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Test ID|rich_text",
              "textContent": "={{ $json.testId }}"
            },
            {
              "key": "Hypothesis|rich_text",
              "textContent": "={{ $json.hypothesis }}"
            },
            {
              "key": "Metric|rich_text",
              "textContent": "={{ $json.metric }}"
            },
            {
              "key": "Status|rich_text",
              "textContent": "running"
            },
            {
              "key": "Campaign ID A|rich_text",
              "textContent": "={{ $json.campaignIdA }}"
            },
            {
              "key": "Campaign ID B|rich_text",
              "textContent": "={{ $json.campaignIdB }}"
            },
            {
              "key": "Start Date|rich_text",
              "textContent": "={{ $json.startDate }}"
            },
            {
              "key": "End Date|rich_text",
              "textContent": "={{ $json.endDate }}"
            },
            {
              "key": "Variant A Subject|rich_text",
              "textContent": "={{ $json.variant_a.subject }}"
            },
            {
              "key": "Variant A Rationale|rich_text",
              "textContent": "={{ $json.variant_a.rationale }}"
            },
            {
              "key": "Variant B Rationale|rich_text",
              "textContent": "={{ $json.variant_b.rationale }}"
            },
            {
              "key": "Variant B Subject|rich_text",
              "textContent": "={{ $json.variant_b.subject }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "ca2fae9a-e9a6-4235-b16e-f221e25fb85e",
      "name": "Daily Poller \u2014 Every 24hrs",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -960,
        4272
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "2b130526-7b42-4053-bc00-f70f01744f74",
      "name": "Notion \u2014 Get Active Tests",
      "type": "n8n-nodes-base.notion",
      "position": [
        -736,
        4272
      ],
      "parameters": {
        "simple": false,
        "options": {},
        "resource": "databasePage",
        "operation": "getAll",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "35ff839f-1184-804c-9b57-c474dc64d6b5",
          "cachedResultUrl": "https://www.notion.so/35ff839f1184804c9b57c474dc64d6b5",
          "cachedResultName": "Email Tracker"
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "5d6009b9-4d15-467e-ada4-eb437e9a700b",
      "name": "Extract Active Tests",
      "type": "n8n-nodes-base.code",
      "position": [
        -512,
        4272
      ],
      "parameters": {
        "jsCode": "const pages = $input.all();\n\nif (!pages || pages.length === 0) {\n  return [];\n}\n\nconst activeTests = [];\n\nfor (const page of pages) {\n  const props = page.json.properties || {};\n  \n  const getTextProp = (key) => {\n    const prop = props[key];\n    if (!prop) return '';\n    if (prop.rich_text && prop.rich_text.length > 0) return prop.rich_text[0].plain_text;\n    if (prop.title && prop.title.length > 0) return prop.title[0].plain_text;\n    return '';\n  };\n  \n  const status = getTextProp('Status');\n  const testId = getTextProp('Test ID');\n  \n  if (status !== 'running') continue;\n  if (!testId) continue;\n  \n  activeTests.push({\n    notionPageId: page.json.id,\n    testId,\n    hypothesis: getTextProp('Hypothesis'),\n    metric: getTextProp('Metric'),\n    campaignIdA: getTextProp('Campaign ID A'),\n    campaignIdB: getTextProp('Campaign ID B'),\n    endDate: getTextProp('End Date'),\n    variantASubject: getTextProp('Variant A Subject'),\n    variantBSubject: getTextProp('Variant B Subject')\n  });\n}\n\nreturn activeTests.map(t => ({ json: t }));"
      },
      "typeVersion": 2
    },
    {
      "id": "f40bdd4f-71ff-429c-94c2-2ee50ee3a337",
      "name": "Significance Test (Chi-Squared)",
      "type": "n8n-nodes-base.code",
      "position": [
        304,
        4272
      ],
      "parameters": {
        "jsCode": "// Merge results from both variants and run statistical significance test\nconst testMeta = $('Extract Active Tests').first().json;\nconst reportA = $('Mailjet \u2014 Poll Results Variant A1').first().json;\nconst reportB = $('Mailjet \u2014 Poll Results Variant B1').first().json;\n\n// Extract stats from Mailjet message response\nconst msgA = reportA.Data?.[0] || {};\nconst msgB = reportB.Data?.[0] || {};\n\nconst sendA = 1;\nconst openA = msgA.IsOpenTracked && msgA.Status === 'opened' ? 1 : 0;\nconst clickA = msgA.IsClickTracked && msgA.Status === 'clicked' ? 1 : 0;\n\nconst sendB = 1;\nconst openB = msgB.IsOpenTracked && msgB.Status === 'opened' ? 1 : 0;\nconst clickB = msgB.IsClickTracked && msgB.Status === 'clicked' ? 1 : 0;\n\nconst metric = testMeta.metric || 'open_rate';\n\nlet successA, successB, trialsA, trialsB;\nif (metric === 'click_rate') {\n  successA = clickA; trialsA = sendA;\n  successB = clickB; trialsB = sendB;\n} else {\n  successA = openA; trialsA = sendA;\n  successB = openB; trialsB = sendB;\n}\n\nconst rateA = trialsA > 0 ? successA / trialsA : 0;\nconst rateB = trialsB > 0 ? successB / trialsB : 0;\n\nconst failA = trialsA - successA;\nconst failB = trialsB - successB;\nconst N = trialsA + trialsB;\n\nlet chiSquared = 0;\nlet isSignificant = false;\nlet winner = null;\nlet confidence = 0;\n\nif (N > 0 && trialsA > 0 && trialsB > 0) {\n  const totalSuccess = successA + successB;\n  const totalFail = failA + failB;\n  const eA_success = (trialsA * totalSuccess) / N;\n  const eA_fail = (trialsA * totalFail) / N;\n  const eB_success = (trialsB * totalSuccess) / N;\n  const eB_fail = (trialsB * totalFail) / N;\n  \n  chiSquared = 0;\n  if (eA_success > 0) chiSquared += Math.pow(successA - eA_success, 2) / eA_success;\n  if (eA_fail > 0) chiSquared += Math.pow(failA - eA_fail, 2) / eA_fail;\n  if (eB_success > 0) chiSquared += Math.pow(successB - eB_success, 2) / eB_success;\n  if (eB_fail > 0) chiSquared += Math.pow(failB - eB_fail, 2) / eB_fail;\n  \n  isSignificant = chiSquared > 3.841;\n  \n  if (chiSquared > 10.828) confidence = 99.9;\n  else if (chiSquared > 7.879) confidence = 99.5;\n  else if (chiSquared > 6.635) confidence = 99;\n  else if (chiSquared > 5.024) confidence = 97.5;\n  else if (chiSquared > 3.841) confidence = 95;\n  else if (chiSquared > 2.706) confidence = 90;\n  else confidence = Math.round(50 + chiSquared * 14.6);\n  \n  if (isSignificant) {\n    winner = rateA >= rateB ? 'A' : 'B';\n  }\n}\n\nconst endDate = new Date(testMeta.endDate);\nconst now = new Date();\nconst testExpired = now > endDate;\n\nlet decision;\nif (isSignificant) {\n  decision = 'declare_winner';\n} else if (testExpired) {\n  decision = 'no_winner_archive';\n} else {\n  decision = 'extend_test';\n}\n\nreturn [{\n  json: {\n    ...testMeta,\n    sendA, openA, clickA, rateA: Math.round(rateA * 10000) / 100,\n    sendB, openB, clickB, rateB: Math.round(rateB * 10000) / 100,\n    statusA: msgA.Status || 'unknown',\n    statusB: msgB.Status || 'unknown',\n    chiSquared: Math.round(chiSquared * 1000) / 1000,\n    isSignificant,\n    confidence,\n    winner,\n    decision,\n    testExpired,\n    analyzedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "240e9dee-1574-4ebb-93fd-630ea8fded86",
      "name": "Route: Winner Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        512,
        4272
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "winner-condition",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.decision }}",
              "rightValue": "declare_winner"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "75cdad42-c57f-4025-a880-a90a5056a2bf",
      "name": "Route: Expired or Extend?",
      "type": "n8n-nodes-base.if",
      "position": [
        736,
        4384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "no-winner-condition",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.decision }}",
              "rightValue": "no_winner_archive"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "2b7e045f-2ce0-49d7-afdc-f99ceaca0ad0",
      "name": "Notion \u2014 Log Winner",
      "type": "n8n-nodes-base.notion",
      "position": [
        736,
        4144
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.notionPageId }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Status|rich_text",
              "textContent": "winner_declared"
            },
            {
              "key": "Winner|rich_text",
              "textContent": "={{ \"Variant \" + $json.winner }}"
            },
            {
              "key": "Rate A|rich_text",
              "textContent": "={{ $json.rateA + \"%\" }}"
            },
            {
              "key": "Rate B|rich_text",
              "textContent": "={{ $json.rateB + \"%\" }}"
            },
            {
              "key": "Chi-Squared|rich_text",
              "textContent": "={{ $json.chiSquared }}"
            },
            {
              "key": "Confidence|rich_text",
              "textContent": "={{ $json.confidence + \"%\" }}"
            },
            {
              "key": "Decision|rich_text",
              "textContent": "={{ \"Winner declared at \" + $json.confidence + \"% confidence\" }}"
            },
            {
              "key": "Analyzed At|rich_text",
              "textContent": "={{ $json.analyzedAt }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "55f2d45f-b302-4759-abd5-23d5fe212306",
      "name": "Identify Loser Campaign",
      "type": "n8n-nodes-base.code",
      "position": [
        960,
        4144
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\n\nconst loserCampaignId = data.winner === 'A' ? data.campaignIdB : data.campaignIdA;\nconst winnerCampaignId = data.winner === 'A' ? data.campaignIdA : data.campaignIdB;\n\nreturn [{\n  json: {\n    ...data,\n    loserCampaignId,\n    winnerCampaignId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "b0e4bf32-441e-4857-b1d2-d57456e3b1e7",
      "name": "Notion \u2014 Log No Winner",
      "type": "n8n-nodes-base.notion",
      "position": [
        1024,
        4400
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.notionPageId }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Status|rich_text",
              "textContent": "no_winner_archived"
            },
            {
              "key": "Decision|rich_text",
              "textContent": "={{ \"Test expired with no significant winner. Chi-squared: \" + $json.chiSquared + \", Confidence: \" + $json.confidence + \"%\" }}"
            },
            {
              "key": "Rate A|rich_text",
              "textContent": "={{ $json.rateA + \"%\" }}"
            },
            {
              "key": "Rate B|rich_text",
              "textContent": "={{ $json.rateB + \"%\" }}"
            },
            {
              "key": "Analyzed At|rich_text",
              "textContent": "={{ $json.analyzedAt }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "81b62eb1-08ba-4623-a6a6-310451456051",
      "name": "Merge1",
      "type": "n8n-nodes-base.merge",
      "position": [
        592,
        3648
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "f94a7281-c79b-49b0-b67b-a7103842e75e",
      "name": "Merge2",
      "type": "n8n-nodes-base.merge",
      "position": [
        32,
        4256
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "21e85667-f91e-406c-bd89-da99f891c1b4",
      "name": "Notion \u2014 Log Extend Test2",
      "type": "n8n-nodes-base.notion",
      "position": [
        1008,
        4608
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.notionPageId }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Status|rich_text",
              "textContent": "running"
            },
            {
              "key": "Decision text|rich_text",
              "textContent": "={{ \"Extending: Chi-squared \" + $json.chiSquared + \" (need >3.841). Rate A: \" + $json.rateA + \"%, Rate B: \" + $json.rateB + \"%. Checked \" + $json.analyzedAt }}"
            },
            {
              "key": "Analyzed at|rich_text",
              "textContent": "={{ $json.analyzedAt }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "230c50b3-d651-4d7c-88fe-0dd7953c9123",
      "name": "\ud83e\udd16 AI: Generate Email Variants (Gemini)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -496,
        6256
      ],
      "parameters": {
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=YOUR_TOKEN_HERE",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"contents\": [\n    {\n      \"parts\": [\n        {\n          \"text\": \"You are a world-class email copywriter specializing in A/B testing. Generate exactly 2 email variants for testing.\\n\\nHypothesis: {{ $json.hypothesis }}\\nMetric to optimize: {{ $json.metric }}\\nEmail Subject Base: {{ $json.emailSubject }}\\n\\nReturn ONLY valid JSON (no markdown, no explanation) in this exact format:\\n{\\n  \\\"variant_a\\\": {\\n    \\\"subject\\\": \\\"subject line for variant A\\\",\\n    \\\"preview_text\\\": \\\"preview text for variant A\\\",\\n    \\\"body_html\\\": \\\"<p>Full HTML email body for variant A.</p>\\\",\\n    \\\"rationale\\\": \\\"why this variant tests the hypothesis\\\"\\n  },\\n  \\\"variant_b\\\": {\\n    \\\"subject\\\": \\\"subject line for variant B\\\",\\n    \\\"preview_text\\\": \\\"preview text for variant B\\\",\\n    \\\"body_html\\\": \\\"<p>Full HTML email body for variant B.</p>\\\",\\n    \\\"rationale\\\": \\\"why this variant contrasts with A\\\"\\n  }\\n}\"\n        }\n      ]\n    }\n  ],\n  \"generationConfig\": {\n    \"temperature\": 0.7,\n    \"maxOutputTokens\": 3000,\n    \"thinkingConfig\": {\n      \"thinkingBudget\": 0\n    }\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "980f05b9-c256-4042-9c14-8ff67732fd46",
      "name": "\ud83d\udcdd Parse Gemini JSON Response",
      "type": "n8n-nodes-base.code",
      "position": [
        -272,
        6256
      ],
      "parameters": {
        "jsCode": "// Parse Gemini's response structure\nconst geminiResponse = $input.first().json;\nconst textContent = geminiResponse.candidates?.[0]?.content?.parts?.[0]?.text;\n\nif (!textContent) throw new Error('No response from Gemini');\n\nlet variants;\ntry {\n  variants = JSON.parse(textContent);\n} catch(e) {\n  const match = textContent.match(/\\{[\\s\\S]*\\}/);\n  if (match) {\n    variants = JSON.parse(match[0]);\n  } else {\n    throw new Error('Could not parse variants JSON from Gemini: ' + textContent);\n  }\n}\n\nconst prevData = $('Parse & Validate Form Data').first().json;\n\nreturn [{\n  json: {\n    ...prevData,\n    variant_a: variants.variant_a,\n    variant_b: variants.variant_b,\n    status: 'variants_generated'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a5d73816-1bc2-4f84-bf07-b57c3a6766d4",
      "name": "\ud83d\udce6 Build Mailjet Email Payloads",
      "type": "n8n-nodes-base.code",
      "position": [
        -32,
        6256
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\n\nconst cleanHtml = (html) => html\n  .replace(/[\\r\\n\\t]+/g, ' ')\n  .trim();\n\nreturn [{\n  json: {\n    ...data,\n    mailjet_payload_a: {\n      FromEmail: data.fromEmail,\n      FromName: data.fromName,\n      Subject: data.variant_a.subject,\n      \"Html-part\": cleanHtml(data.variant_a.body_html),\n      Recipients: [{ Email: data.fromEmail }],\n      Headers: {\n        \"Reply-To\": data.fromEmail\n      }\n    },\n    mailjet_payload_b: {\n      FromEmail: data.fromEmail,\n      FromName: data.fromName,\n      Subject: data.variant_b.subject,\n      \"Html-part\": cleanHtml(data.variant_b.body_html),\n      Recipients: [{ Email: data.fromEmail }],\n      Headers: {\n        \"Reply-To\": data.fromEmail\n      }\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "25fa41e9-1858-4b54-acb2-945a63e7b1e7",
      "name": "\ud83d\udce7 Send Email \u2014 Variant A",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        208,
        6144
      ],
      "parameters": {
        "url": "https://api.mailjet.com/v3/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.mailjet_payload_a) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Basic ODYyNDhiMjY4ZWE5ZWQ3YzFhYjU0Yjc2MGIxYjc5OTA6MmIxNmNhNzU2NWViYmI2ZDU4NDNhYzFhMjhmNTIyYmM="
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "d9dd578a-2dfe-4b2e-8d98-e8cf57fb9a92",
      "name": "\ud83d\udce7 Send Email \u2014 Variant B",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        208,
        6384
      ],
      "parameters": {
        "url": "https://api.mailjet.com/v3/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.mailjet_payload_b) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Basic ODYyNDhiMjY4ZWE5ZWQ3YzFhYjU0Yjc2MGIxYjc5OTA6MmIxNmNhNzU2NWViYmI2ZDU4NDNhYzFhMjhmNTIyYmM="
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "f3f47e16-d084-4e85-819b-b545387c92bd",
      "name": "\ud83d\udcca Poll Mailjet \u2014 Variant A Stats",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -208,
        7200
      ],
      "parameters": {
        "url": "=https://api.mailjet.com/v3/REST/message?MessageUUID={{ $json.campaignIdA }}",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Basic ODYyNDhiMjY4ZWE5ZWQ3YzFhYjU0Yjc2MGIxYjc5OTA6MmIxNmNhNzU2NWViYmI2ZDU4NDNhYzFhMjhmNTIyYmM="
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "9b2fa353-7e2d-4e4b-a8f3-60199298f883",
      "name": "\ud83d\udcca Poll Mailjet \u2014 Variant B Stats",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -208,
        7360
      ],
      "parameters": {
        "url": "=https://api.mailjet.com/v3/REST/message?MessageUUID={{ $json.campaignIdB }}",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Basic ODYyNDhiMjY4ZWE5ZWQ3YzFhYjU0Yjc2MGIxYjc5OTA6MmIxNmNhNzU2NWViYmI2ZDU4NDNhYzFhMjhmNTIyYmM="
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "e856ede1-501c-44b9-8d8e-ca97e7e5a804",
      "name": "\ud83d\uddd1\ufe0f Log Loser Campaign UUID (Audit)",
      "type": "n8n-nodes-base.code",
      "position": [
        1264,
        7152
      ],
      "parameters": {
        "jsCode": "// Mailjet doesn't have an archive API \u2014 log the loser UUID for reference\nconst data = $input.first().json;\nconsole.log('Loser campaign UUID:', data.loserCampaignId);\nconsole.log('Winner campaign UUID:', data.winnerCampaignId);\nreturn [{ json: { ...data, archivedAt: new Date().toISOString() } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "c667abd2-7c68-478f-8312-7c07ef2048c2",
      "name": "\ud83d\udce5 Watch Form Submissions (Google Sheets)",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "position": [
        -944,
        6256
      ],
      "parameters": {
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1pKhSTA4_KQfiEA69LNoY5voPvpN7vaCaYtHIVzv2EI0/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1pKhSTA4_KQfiEA69LNoY5voPvpN7vaCaYtHIVzv2EI0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1pKhSTA4_KQfiEA69LNoY5voPvpN7vaCaYtHIVzv2EI0/edit?usp=drivesdk",
          "cachedResultName": "Email responses"
        }
      },
      "credentials": {
        "googleSheetsTriggerOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "88f50115-9d2b-402d-8041-43c894b5edcd",
      "name": "\ud83d\udd0d Parse & Validate Input Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -720,
        6256
      ],
      "parameters": {
        "jsCode": "// Extract and validate incoming form data\nconst body = $input.first().json.body || $input.first().json;\n\nconst hypothesis = body.hypothesis || body.field_1 || '';\nconst metric = body.metric || body.field_2 || 'open_rate';\nconst emailSubject = body.email_subject || body.field_3 || 'Test Email';\nconst audienceSize = body.audience_size || 200;\nconst testDurationDays = body.test_duration_days || 7;\nconst fromEmail = body.from_email || 'user@example.com';\nconst fromName = body.from_name || 'Techdome';\nconst listId = body.list_id || 4;\n\nif (!hypothesis) throw new Error('Missing hypothesis in form data');\nif (!metric) throw new Error('Missing metric in form data');\n\nconst testId = 'ab_' + Date.now();\nconst startDate = new Date().toISOString();\nconst endDate = new Date(Date.now() + testDurationDays * 86400000).toISOString();\n\nreturn [{\n  json: {\n    testId,\n    hypothesis,\n    metric,\n    emailSubject,\n    audienceSize,\n    testDurationDays,\n    fromEmail,\n    fromName,\n    listId,\n    startDate,\n    endDate,\n    status: 'generating_variants'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "bb11a65a-7ece-4246-b4c6-021ea2d4eb1e",
      "name": "\ud83d\udd17 Extract & Merge Campaign IDs",
      "type": "n8n-nodes-base.code",
      "position": [
        736,
        6256
      ],
      "parameters": {
        "jsCode": "// Merge MessageIDs from both Mailjet sends\nconst variantAResponse = $('Mailjet \u2014 Send Variant A1').first().json;\nconst variantBResponse = $('Mailjet \u2014 Send Variant B1').first().json;\nconst parsedData = $('Prepare Mailjet Payloads1').first().json;\n\nconst campaignIdA = variantAResponse.Sent?.[0]?.MessageUUID;\nconst campaignIdB = variantBResponse.Sent?.[0]?.MessageUUID;\nconst messageIdA = variantAResponse.Sent?.[0]?.MessageID;\nconst messageIdB = variantBResponse.Sent?.[0]?.MessageID;\n\nif (!campaignIdA || !campaignIdB) {\n  throw new Error('Failed to send Mailjet emails. Response A: ' + JSON.stringify(variantAResponse) + ', Response B: ' + JSON.stringify(variantBResponse));\n}\n\nreturn [{\n  json: {\n    ...parsedData,\n    campaignIdA,\n    campaignIdB,\n    messageIdA,\n    messageIdB,\n    status: 'campaigns_sent'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "479920ae-ef42-45ef-9e8a-53dc656f75b1",
      "name": "\ud83d\udcd3 Log Test Start to Notion",
      "type": "n8n-nodes-base.notion",
      "position": [
        992,
        6256
      ],
      "parameters": {
        "title": "=A/B Test: {{ $json.testId }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "35ff839f-1184-804c-9b57-c474dc64d6b5",
          "cachedResultUrl": "https://www.notion.so/35ff839f1184804c9b57c474dc64d6b5",
          "cachedResultName": "Email Tracker"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Test ID|rich_text",
              "textContent": "={{ $json.testId }}"
            },
            {
              "key": "Hypothesis|rich_text",
              "textContent": "={{ $json.hypothesis }}"
            },
            {
              "key": "Metric|rich_text",
              "textContent": "={{ $json.metric }}"
            },
            {
              "key": "Status|rich_text",
              "textContent": "running"
            },
            {
              "key": "Campaign ID A|rich_text",
              "textContent": "={{ $json.campaignIdA }}"
            },
            {
              "key": "Campaign ID B|rich_text",
              "textContent": "={{ $json.campaignIdB }}"
            },
            {
              "key": "Start Date|rich_text",
              "textContent": "={{ $json.startDate }}"
            },
            {
              "key": "End Date|rich_text",
              "textContent": "={{ $json.endDate }}"
            },
            {
              "key": "Variant A Subject|rich_text",
              "textContent": "={{ $json.variant_a.subject }}"
            },
            {
              "key": "Variant A Rationale|rich_text",
              "textContent": "={{ $json.variant_a.rationale }}"
            },
            {
              "key": "Variant B Rationale|rich_text",
              "textContent": "={{ $json.variant_b.rationale }}"
            },
            {
              "key": "Variant B Subject|rich_text",
              "textContent": "={{ $json.variant_b.subject }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "290b8187-76fb-45e4-a65a-89e00cc6858f",
      "name": "\u23f0 Daily Poller \u2014 Runs Every 24 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -880,
        7280
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "135c478e-4864-4907-bda4-1ec6c33be44b",
      "name": "\ud83d\udccb Fetch All Running Tests from Notion",
      "type": "n8n-nodes-base.notion",
      "position": [
        -656,
        7280
      ],
      "parameters": {
        "simple": false,
        "options": {},
        "resource": "databasePage",
        "operation": "getAll",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "35ff839f-1184-804c-9b57-c474dc64d6b5",
          "cachedResultUrl": "https://www.notion.so/35ff839f1184804c9b57c474dc64d6b5",
          "cachedResultName": "Email Tracker"
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "9b9551ef-3260-4e33-bcf4-1e0468dbf60b",
      "name": "\ud83d\uddc2\ufe0f Filter & Extract Active Test Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -432,
        7280
      ],
      "parameters": {
        "jsCode": "const pages = $input.all();\n\nif (!pages || pages.length === 0) {\n  return [];\n}\n\nconst activeTests = [];\n\nfor (const page of pages) {\n  const props = page.json.properties || {};\n  \n  const getTextProp = (key) => {\n    const prop = props[key];\n    if (!prop) return '';\n    if (prop.rich_text && prop.rich_text.length > 0) return prop.rich_text[0].plain_text;\n    if (prop.title && prop.title.length > 0) return prop.title[0].plain_text;\n    return '';\n  };\n  \n  const status = getTextProp('Status');\n  const testId = getTextProp('Test ID');\n  \n  if (status !== 'running') continue;\n  if (!testId) continue;\n  \n  activeTests.push({\n    notionPageId: page.json.id,\n    testId,\n    hypothesis: getTextProp('Hypothesis'),\n    metric: getTextProp('Metric'),\n    campaignIdA: getTextProp('Campaign ID A'),\n    campaignIdB: getTextProp('Campaign ID B'),\n    endDate: getTextProp('End Date'),\n    variantASubject: getTextProp('Variant A Subject'),\n    variantBSubject: getTextProp('Variant B Subject')\n  });\n}\n\nreturn activeTests.map(t => ({ json: t }));"
      },
      "typeVersion": 2
    },
    {
      "id": "d72036e4-2bdf-4827-bdc5-99f60af6d077",
      "name": "\ud83d\udcd0 Chi-Squared Statistical Significance Test",
      "type": "n8n-nodes-base.code",
      "position": [
        384,
        7280
      ],
      "parameters": {
        "jsCode": "// Merge results from both variants and run statistical significance test\nconst testMeta = $('Extract Active Tests').first().json;\nconst reportA = $('Mailjet \u2014 Poll Results Variant A1').first().json;\nconst reportB = $('Mailjet \u2014 Poll Results Variant B1').first().json;\n\n// Extract stats from Mailjet message response\nconst msgA = reportA.Data?.[0] || {};\nconst msgB = reportB.Data?.[0] || {};\n\nconst sendA = 1;\nconst openA = msgA.IsOpenTracked && msgA.Status === 'opened' ? 1 : 0;\nconst clickA = msgA.IsClickTracked && msgA.Status === 'clicked' ? 1 : 0;\n\nconst sendB = 1;\nconst openB = msgB.IsOpenTracked && msgB.Status === 'opened' ? 1 : 0;\nconst clickB = msgB.IsClickTracked && msgB.Status === 'clicked' ? 1 : 0;\n\nconst metric = testMeta.metric || 'open_rate';\n\nlet successA, successB, trialsA, trialsB;\nif (metric === 'click_rate') {\n  successA = clickA; trialsA = sendA;\n  successB = clickB; trialsB = sendB;\n} else {\n  successA = openA; trialsA = sendA;\n  successB = openB; trialsB = sendB;\n}\n\nconst rateA = trialsA > 0 ? successA / trialsA : 0;\nconst rateB = trialsB > 0 ? successB / trialsB : 0;\n\nconst failA = trialsA - successA;\nconst failB = trialsB - successB;\nconst N = trialsA + trialsB;\n\nlet chiSquared = 0;\nlet isSignificant = false;\nlet winner = null;\nlet confidence = 0;\n\nif (N > 0 && trialsA > 0 && trialsB > 0) {\n  const totalSuccess = successA + successB;\n  const totalFail = failA + failB;\n  const eA_success = (trialsA * totalSuccess) / N;\n  const eA_fail = (trialsA * totalFail) / N;\n  const eB_success = (trialsB * totalSuccess) / N;\n  const eB_fail = (trialsB * totalFail) / N;\n  \n  chiSquared = 0;\n  if (eA_success > 0) chiSquared += Math.pow(successA - eA_success, 2) / eA_success;\n  if (eA_fail > 0) chiSquared += Math.pow(failA - eA_fail, 2) / eA_fail;\n  if (eB_success > 0) chiSquared += Math.pow(successB - eB_success, 2) / eB_success;\n  if (eB_fail > 0) chiSquared += Math.pow(failB - eB_fail, 2) / eB_fail;\n  \n  isSignificant = chiSquared > 3.841;\n  \n  if (chiSquared > 10.828) confidence = 99.9;\n  else if (chiSquared > 7.879) confidence = 99.5;\n  else if (chiSquared > 6.635) confidence = 99;\n  else if (chiSquared > 5.024) confidence = 97.5;\n  else if (chiSquared > 3.841) confidence = 95;\n  else if (chiSquared > 2.706) confidence = 90;\n  else confidence = Math.round(50 + chiSquared * 14.6);\n  \n  if (isSignificant) {\n    winner = rateA >= rateB ? 'A' : 'B';\n  }\n}\n\nconst endDate = new Date(testMeta.endDate);\nconst now = new Date();\nconst testExpired = now > endDate;\n\nlet decision;\nif (isSignificant) {\n  decision = 'declare_winner';\n} else if (testExpired) {\n  decision = 'no_winner_archive';\n} else {\n  decision = 'extend_test';\n}\n\nreturn [{\n  json: {\n    ...testMeta,\n    sendA, openA, clickA, rateA: Math.round(rateA * 10000) / 100,\n    sendB, openB, clickB, rateB: Math.round(rateB * 10000) / 100,\n    statusA: msgA.Status || 'unknown',\n    statusB: msgB.Status || 'unknown',\n    chiSquared: Math.round(chiSquared * 1000) / 1000,\n    isSignificant,\n    confidence,\n    winner,\n    decision,\n    testExpired,\n    analyzedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4ac85071-89ae-4633-89b0-3adb7a3c12de",
      "name": "\ud83d\udea6 Decision: Winner Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        592,
        7280
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "winner-condition",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.decision }}",
              "rightValue": "declare_winner"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "3e7ffd83-db62-4570-9882-432ae234cbfc",
      "name": "\ud83d\udea6 Decision: Test Expired or Extend?",
      "type": "n8n-nodes-base.if",
      "position": [
        816,
        7392
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "no-winner-condition",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.decision }}",
              "rightValue": "no_winner_archive"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "a690c742-20d7-451c-826c-9284b8400d0e",
      "name": "\ud83c\udfc6 Update Notion \u2014 Winner Declared",
      "type": "n8n-nodes-base.notion",
      "position": [
        816,
        7152
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.notionPageId }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Status|rich_text",
              "textContent": "winner_declared"
            },
            {
              "key": "Winner|rich_text",
              "textContent": "={{ \"Variant \" + $json.winner }}"
            },
            {
              "key": "Rate A|rich_text",
              "textContent": "={{ $json.rateA + \"%\" }}"
            },
            {
              "key": "Rate B|rich_text",
              "textContent": "={{ $json.rateB + \"%\" }}"
            },
            {
              "key": "Chi-Squared|rich_text",
              "textContent": "={{ $json.chiSquared }}"
            },
            {
              "key": "Confidence|rich_text",
              "textContent": "={{ $json.confidence + \"%\" }}"
            },
            {
              "key": "Decision|rich_text",
              "textContent": "={{ \"Winner declared at \" + $json.confidence + \"% confidence\" }}"
            },
            {
              "key": "Analyzed At|rich_text",
              "textContent": "={{ $json.analyzedAt }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "8cac62b2-ffe0-4348-984e-9974b244030f",
      "name": "\ud83d\udd0e Identify Losing Campaign",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        7152
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\n\nconst loserCampaignId = data.winner === 'A' ? data.campaignIdB : data.campaignIdA;\nconst winnerCampaignId = data.winner === 'A' ? data.campaignIdA : data.campaignIdB;\n\nreturn [{\n  json: {\n    ...data,\n    loserCampaignId,\n    winnerCampaignId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4fc937ee-6f25-483f-b4c8-5ed9beb0089f",
      "name": "\ud83d\udced Update Notion \u2014 No Winner (Archived)",
      "type": "n8n-nodes-base.notion",
      "position": [
        1104,
        7408
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.notionPageId }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Status|rich_text",
              "textContent": "no_winner_archived"
            },
            {
              "key": "Decision|rich_text",
              "textContent": "={{ \"Test expired with no significant winner. Chi-squared: \" + $json.chiSquared + \", Confidence: \" + $json.confidence + \"%\" }}"
            },
            {
              "key": "Rate A|rich_text",
              "textContent": "={{ $json.rateA + \"%\" }}"
            },
            {
              "key": "Rate B|rich_text",
              "textContent": "={{ $json.rateB + \"%\" }}"
            },
            {
              "key": "Analyzed At|rich_text",
              "textContent": "={{ $json.analyzedAt }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "22c0637f-7441-43b4-b40c-3b78b3710109",
      "name": "\ud83d\udd00 Merge Send Confirmations",
      "type": "n8n-nodes-base.merge",
      "position": [
        528,
        6256
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "246f0850-38b7-496e-bcec-fcd582f8e53b",
      "name": "\ud83d\udd00 Merge Poll Results (A + B)",
      "type": "n8n-nodes-base.merge",
      "position": [
        112,
        7264
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "a41c0ea0-87ff-4b29-906c-af31ed1b3249",
      "name": "\ud83d\udd01 Update Notion \u2014 Test Extended",
      "type": "n8n-nodes-base.notion",
      "position": [
        1088,
        7616
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.notionPageId }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Status|rich_text",
              "textContent": "running"
            },
            {
              "key": "Decision text|rich_text",
              "textContent": "={{ \"Extending: Chi-squared \" + $json.chiSquared + \" (need >3.841). Rate A: \" + $json.rateA + \"%, Rate B: \" + $json.rateB + \"%. Checked \" + $json.analyzedAt }}"
            },
            {
              "key": "Analyzed at|rich_text",
              "textContent": "={{ $json.analyzedAt }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "52ad60e8-e26d-470b-bedb-815335c05af7",
      "name": "\ud83d\uddd2\ufe0f Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2016,
        6672
      ],
      "parameters": {
        "color": 4,
        "width": 788,
        "height": 152,
        "content": "# \ud83e\uddea Autonomous A/B Email Test Orchestrator\nThis workflow has **two independent sub-flows**: (1) **Launch Pipeline** \u2014 triggered by a Google Sheets form submission to generate & send A/B email variants via Gemini AI + Mailjet, then log to Notion. (2) **Daily Analysis Engine** \u2014 a scheduled poller that fetches running tests from Notion, polls Mailjet stats, runs a Chi-Squared significance test, and automatically declares a winner, extends, or archives the test."
      },
      "typeVersion": 1
    },
    {
      "id": "29a86b84-b1d7-41df-92c1-97f146e3699a",
      "name": "\ud83d\uddd2\ufe0f Launch Pipeline Header",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1072,
        6016
      ],
      "parameters": {
        "color": 3,
        "width": 2260,
        "content": "## \ud83d\ude80 LAUNCH PIPELINE \u2014 Triggered by Google Sheets Form Submission\nWhen a new row appears in the **Email responses** sheet, this pipeline: validates input \u2192 calls Gemini AI \u2192 generates 2 email variants \u2192 sends both via Mailjet \u2192 captures Campaign UUIDs \u2192 creates a new `running` test entry in the **Notion Email Tracker** database."
      },
      "typeVersion": 1
    },
    {
      "id": "e7a2e010-ed45-40ab-a4e3-8117f3ba8d74",
      "name": "\ud83d\uddd2\ufe0f Google Sheets Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1312,
        6256
      ],
      "parameters": {
        "color": 6,
        "width": 300,
        "height": 252,
        "content": "### \ud83d\udce5 Form Trigger\nPolls the **Email responses** Google Sheet every minute.\n\nRequired columns:\n- `hypothesis`\n- `metric` (open_rate / click_rate)\n- `email_subject`\n- `from_email`, `from_name`\n- `audience_size`, `test_duration_days`"
      },
      "typeVersion": 1
    },
    {
      "id": "ea6a7bee-e0bc-4887-bea4-2941427f7b00",
      "name": "\ud83d\uddd2\ufe0f Validate Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -832,
        6448
      ],
      "parameters": {
        "color": 6,
        "width": 280,
        "height": 216,
        "content": "### \ud83d\udd0d Input Validation\nExtracts and validates all form fields.\n\nGenerates a unique `testId` (`ab_<timestamp>`), calculates `startDate` and `endDate` based on `test_duration_days`.\n\n\u26a0\ufe0f Throws error if `hypothesis` or `metric` is missing."
      },
      "typeVersion": 1
    },
    {
      "id": "10cd6981-777d-44c6-90d3-3e64bf6ba2cb",
      "name": "\ud83d\uddd2\ufe0f Gemini Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -448,
        6432
      ],
      "parameters": {
        "color": 2,
        "width": 300,
        "height": 272,
        "content": "### \ud83e\udd16 Gemini AI \u2014 Variant Generation\nCalls **Gemini 2.5 Flash** with the hypothesis, optimisation metric, and base subject line.\n\nReturns 2 variants as JSON:\n```\n{ subject, preview_text,\n  body_html, rationale }\n```\nTemp: `0.7` \u00b7 Max tokens: `3000`\nThinking budget: `0` (fast mode)"
      },
      "typeVersion": 1
    },
    {
      "id": "ab580b3b-4562-496f-b9f0-2f26b54b845e",
      "name": "\ud83d\uddd2\ufe0f Mailjet Send Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        384,
        6448
      ],
      "parameters": {
        "color": 2,
        "width": 300,
        "height": 248,
        "content": "### \ud83d\udce7 Mailjet \u2014 Parallel Send\nBoth Variant A and Variant B are sent **simultaneously** via the Mailjet v3 `/send` API.\n\nEach response captures:\n- `MessageUUID` \u2192 used as `campaignIdA/B` for polling\n- `MessageID` \u2192 stored for reference\n\nAuth: Base64-encoded API key."
      },
      "typeVersion": 1
    },
    {
      "id": "85625247-6853-4d7c-92c9-56d4de2b22f0",
      "name": "\ud83d\uddd2\ufe0f Notion Log Start Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        

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

Automate your entire email experimentation workflow with this AI-powered A/B testing orchestrator 🚀. This n8n automation generates two intelligent email variants using AI, sends campaigns through Mailjet, continuously tracks engagement performance, and automatically determines…

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

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

This workflow triggers when a HubSpot deal stage changes to Closed Won and automatically generates an invoice. It collects deal and contact data, builds a styled invoice, converts it into a PDF, and s

HubSpot Trigger, HTTP Request, Google Sheets +4
Data & Sheets

Description

HTTP Request, Google Sheets, ClickUp +1
Data & Sheets

Stickynote Workflow. Uses googleTranslate, googleSheetsTrigger, googleDrive, httpRequest. Event-driven trigger; 22 nodes.

Google Translate, Google Sheets Trigger, Google Drive +2
Data & Sheets

Automate IT asset allocation for new hires with an intelligent, AI-powered workflow 🤖. This automation reads employee data from Google Sheets, determines role-based requirements, and intelligently ass

Google Sheets Trigger, Airtable, HTTP Request +2
Data & Sheets

Generate market research reports from news and competitor sites to Notion and Slack. Uses errorTrigger, httpRequest, notion, googleSheets. Event-driven trigger; 19 nodes.

Error Trigger, HTTP Request, Notion +2