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 →
{
"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.
googleSheetsTriggerOAuth2ApihttpHeaderAuthnotionApi
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.
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
Description
Stickynote Workflow. Uses googleTranslate, googleSheetsTrigger, googleDrive, httpRequest. Event-driven trigger; 22 nodes.
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
Generate market research reports from news and competitor sites to Notion and Slack. Uses errorTrigger, httpRequest, notion, googleSheets. Event-driven trigger; 19 nodes.