{
  "id": "8JhgqruUfb60CaUs",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Synthetic Focus Group",
  "tags": [],
  "nodes": [
    {
      "id": "401a2c5e-568a-4552-8174-762804fd1580",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 1040,
        "height": 1520,
        "content": "## Synthetic Focus Group\n\nThis workflow tests a marketing campaign against a panel of AI personas built from your ICP (Ideal Customer Profile) segments, before you spend a cent on media. It simulates how distinct customer segments would actually react to the copy, aggregates that into a transparent, deterministic appeal score, and has AI synthesize the panel's reactions into a launch recommendation \u2014 with every test logged for tracking campaign quality over time.\n\nThe appeal score is deliberately NOT left entirely to the AI. Purchase intent, clarity, sentiment, objection density, and share rate \u2014 each weighted by how important that ICP segment is to you \u2014 are combined into a numeric 0-100 score with a plain formula in code, so the number behind every recommendation is explainable and reproducible. AI is used where it adds real value: role-playing distinct, in-character persona reactions to your actual copy, and writing a clear, human-readable synthesis and set of suggested edits on top of that score.\n\n### Who's it for\n- Growth and lifecycle marketers who want a fast gut-check on campaign copy before it goes to a media buy or send\n- Brand and creative teams who want to catch confusing offers or off-tone copy before customers do\n- Anyone replacing informal \"send it to the team Slack channel\" campaign reviews with a structured, repeatable test\n- Teams who want a lightweight, directional pre-test when a real customer panel or A/B test isn't feasible for every campaign\n\n### How it works\n1. Marketer submits campaign copy, channel, offer details, brand voice, and their ICP segments (with relative importance weights) via webhook POST\n2. Code builds a persona panel from those ICP segments, normalizing the segment weights so they sum to 1\n3. AI role-plays each persona reacting in-character to the actual campaign copy \u2014 sentiment, purchase intent, clarity, objections, and whether they'd share it \u2014 and reports its own confidence that the campaign copy was clear enough to simulate against\n4. Campaigns too thin or garbled to simulate confidently are sent straight back for clearer copy, instead of generating a false-confidence score\n5. Code combines every persona's reaction, weighted by that segment's importance, into a composite 0-100 appeal score and readiness tier\n6. AI reviews the aggregate scores and every persona's actual reaction and writes a synthesis: an overall recommendation (Launch As-Is, Launch with Minor Edits, Revise Before Launch, Do Not Launch), an executive summary, strengths, risks, and specific suggested copy changes\n7. Every test is logged to Google Sheets regardless of outcome, building a history of campaign quality over time\n8. Launch-ready campaigns get a notification to the growth marketing team; anything needing work notifies the creative team with the specific suggested changes\n9. Webhook responds with the full report, composite score, and per-persona breakdown as JSON\n\n### How to set up\n1. Import this workflow into n8n\n2. Add Anthropic Claude credentials and attach to both Anthropic model nodes\n3. Add SendGrid credentials and replace YOUR_SENDGRID_API_KEY in both email nodes\n4. Replace growth-marketing@youragency.com and creative-team@youragency.com with your real team addresses\n5. Switch the Google Sheets HTTP node to the native Google Sheets node with OAuth2, or add OAuth2 credentials directly\n6. Replace YOUR_SHEET_ID with your actual Spreadsheet ID\n7. Set up your Google Sheet with a FocusGroupReports tab (column layout below)\n8. Activate and POST a test campaign to the webhook URL\n\n### Requirements\n- Anthropic Claude API key (claude-sonnet-4 recommended)\n- SendGrid API key for email delivery\n- Google Sheets with OAuth2 credentials (FocusGroupReports tab)\n- n8n instance with a publicly accessible webhook URL\n\n### How to customize\n- Adjust the scoring weights and readiness-tier thresholds in JS - Calculate Aggregate Campaign Scores to match how your team values purchase intent vs. clarity vs. sentiment\n- Raise or lower the simulation-confidence gate in IF - Panel Reactions Usable (default: confidence >= 4)\n- Add more or fewer ICP segments in the icpSegmentsJson payload field \u2014 the persona panel scales automatically\n- Feed real historical campaign performance back in as a weighting input so the model calibrates against your actual results over time\n- Add a second synthesis pass focused specifically on subject-line or CTA variants for A/B testing\n\n### Important note\nThis workflow produces a directional, pre-spend signal to support human marketers, not a guarantee of real-world campaign performance. Synthetic personas are a proxy for real customer reactions, not a replacement for actual user testing, and should be treated as one input among several before a launch decision.\n\n### Google Sheets Column Layout\n\nFocusGroupReports tab - row 1 headers:\nCampaign ID | Campaign Name | Channel | Budget At Stake | Panel Size | Composite Appeal Score | Readiness Tier | Weighted Purchase Intent | Weighted Clarity | Overall Recommendation | Top Risks | Decided At"
      },
      "typeVersion": 1
    },
    {
      "id": "0b3ec590-7268-4ff1-82bc-87dcf69d3bc0",
      "name": "Sticky Note - Stage 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        496
      ],
      "parameters": {
        "color": 4,
        "width": 1588,
        "height": 820,
        "content": "## Stage 1: Intake, Persona Panel Build & Simulated Reactions\n\nAccepts a campaign submission via webhook POST, including the raw campaign copy, channel, brand voice, offer details, and a JSON array of ICP segments with relative importance weights. All fields have sensible defaults so the workflow can be tested without a payload.\n\nJS - Build Persona Panel is pure code, not AI: it parses the ICP segments, falls back to two generic segments if the input is missing or malformed, and normalizes every segment's weight so they sum to 1.\n\nThe persona-reaction AI (claude-sonnet-4, temperature 0.7 for varied, distinct voices) role-plays each persona in the panel reacting in-character to the actual campaign copy, and reports its own confidence that the copy was clear enough to simulate against.\n\nJS - Parse Persona Reactions Output extracts the AI JSON with a fallback that builds neutral placeholder reactions (and a low confidence score) if parsing fails, so a bad AI response can't silently masquerade as a real signal.\n\nIF - Panel Reactions Usable sends campaigns straight back for clearer copy when the AI's own simulation confidence is too low, instead of scoring against a shaky simulation."
      },
      "typeVersion": 1
    },
    {
      "id": "f53726aa-1ece-4172-9704-650dc94235dc",
      "name": "Sticky Note - Stage 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2768,
        448
      ],
      "parameters": {
        "color": 4,
        "width": 1092,
        "height": 820,
        "content": "## Stage 2: Aggregate Scoring & AI Synthesis\n\nJS - Calculate Aggregate Campaign Scores is pure code, not AI: it combines every persona's purchase intent, clarity, sentiment, objections, and share likelihood \u2014 weighted by that segment's importance \u2014 into a composite 0-100 appeal score and readiness tier, and surfaces the most frequently repeated objections across the panel.\n\nA Wait buffer respects API rate limits before the next AI call.\n\nThe synthesis AI (claude-sonnet-4, temperature 0.3 for consistent, grounded judgment) reviews the composite score, the readiness tier, and every persona's actual reaction, then writes an overall recommendation, an executive summary, strengths, risks, and specific suggested copy changes.\n\nJS - Parse Synthesis Output & Format Report Record extracts that JSON with a conservative fallback: if parsing fails, the campaign defaults to Revise Before Launch rather than a silent Launch As-Is, and also assembles the summary row for the tracker sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "6cbb8712-18b3-4832-9004-cd296e304945",
      "name": "Sticky Note - Stage 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3904,
        224
      ],
      "parameters": {
        "color": 4,
        "width": 1324,
        "height": 1066,
        "content": "## Stage 3: Logging & Recommendation Delivery\n\nA final Wait buffer respects downstream API rate limits before logging and delivery.\n\nSave Focus Group Report to Google Sheets Tracker appends the summary row \u2014 every test is logged, regardless of outcome, building a history of campaign quality over time.\n\nIF - Launch Ready branches on the AI's overall recommendation:\n- Launch As-Is or Launch with Minor Edits: Send Campaign Ready Notification Email lets the growth marketing team know the campaign tested well, then the webhook responds with the full report.\n- Revise Before Launch or Do Not Launch: Send Revision Required Notification Email gives the creative team the executive summary, top risks, and suggested copy changes, and the webhook responds with the same report detail \u2014 both fire in parallel."
      },
      "typeVersion": 1
    },
    {
      "id": "a13b88a1-9b45-4b11-8932-21505caba06c",
      "name": "Webhook - Receive Campaign Submission",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1184,
        928
      ],
      "parameters": {
        "path": "synthetic-focus-group-inbound",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 1.1
    },
    {
      "id": "d170ee9d-7f0f-4185-83e3-606c80fa96e4",
      "name": "Set - Prepare Campaign Context",
      "type": "n8n-nodes-base.set",
      "position": [
        1408,
        928
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "campaignName",
              "type": "string",
              "value": "={{ $json.campaignName || $json.body?.campaignName || 'Summer Loyalty Relaunch \u2013 Email Campaign' }}"
            },
            {
              "name": "campaignChannel",
              "type": "string",
              "value": "={{ $json.campaignChannel || $json.body?.campaignChannel || 'Email' }}"
            },
            {
              "name": "campaignCopy",
              "type": "string",
              "value": "={{ $json.campaignCopy || $json.body?.campaignCopy || 'Subject: Your VIP Summer Perks Are Here. Body: You have earned it, unlock 25% off your next order plus free shipping, just for being one of our best customers. Perks this good do not last long.' }}"
            },
            {
              "name": "campaignCTA",
              "type": "string",
              "value": "={{ $json.campaignCTA || $json.body?.campaignCTA || 'Shop the Sale' }}"
            },
            {
              "name": "offerDetails",
              "type": "string",
              "value": "={{ $json.offerDetails || $json.body?.offerDetails || '25% off sitewide plus free shipping, valid 5 days, loyalty members only' }}"
            },
            {
              "name": "brandVoice",
              "type": "string",
              "value": "={{ $json.brandVoice || $json.body?.brandVoice || 'Friendly, energetic, slightly cheeky' }}"
            },
            {
              "name": "icpSegmentsJson",
              "type": "string",
              "value": "={{ $json.icpSegmentsJson || $json.body?.icpSegmentsJson || '[{\"segmentName\":\"Budget-Conscious Parent\",\"weight\":0.35,\"description\":\"35-45 year old parent, shops sales and coupons, price-sensitive, values family-friendly framing\",\"painPoints\":[\"Limited discretionary spend\",\"Skeptical of urgency tactics\"]},{\"segmentName\":\"Loyalty Power User\",\"weight\":0.3,\"description\":\"25-40 year old repeat customer already enrolled in the loyalty program, motivated by status and perks\",\"painPoints\":[\"Fatigue from frequent promo emails\",\"Wants to feel recognized, not spammed\"]},{\"segmentName\":\"Gen Z Discovery Shopper\",\"weight\":0.2,\"description\":\"18-24 year old who discovered the brand via social media, price-sensitive but values authenticity over discounts\",\"painPoints\":[\"Distrusts overly salesy copy\",\"Short attention span\"]},{\"segmentName\":\"Premium Loyalist\",\"weight\":0.15,\"description\":\"45-60 year old high-spend repeat customer, less price-driven, values quality and feeling like a VIP\",\"painPoints\":[\"Dislikes generic mass-market language\",\"Wants exclusivity, not mass discounting\"]}]' }}"
            },
            {
              "name": "budgetAtStake",
              "type": "number",
              "value": "={{ $json.budgetAtStake || $json.body?.budgetAtStake || 8000 }}"
            },
            {
              "name": "launchDate",
              "type": "string",
              "value": "={{ $json.launchDate || $json.body?.launchDate || '2026-07-28' }}"
            },
            {
              "name": "requestedBy",
              "type": "string",
              "value": "={{ $json.requestedBy || $json.body?.requestedBy || 'Growth Marketing Team' }}"
            },
            {
              "name": "submittedAt",
              "type": "string",
              "value": "={{ new Date().toISOString() }}"
            },
            {
              "name": "campaignId",
              "type": "string",
              "value": "={{ 'CMP-' + Date.now().toString().slice(-8) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a3109a51-271a-4d4a-8783-72c17c48571c",
      "name": "JS - Build Persona Panel",
      "type": "n8n-nodes-base.code",
      "position": [
        1632,
        928
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nlet segments = [];\ntry {\n  segments = JSON.parse(item.icpSegmentsJson || '[]');\n  if (!Array.isArray(segments) || segments.length === 0) throw new Error('empty');\n} catch (e) {\n  segments = [\n    { segmentName: 'Value-Driven Generalist', weight: 0.5, description: 'Price-sensitive shopper who compares offers before buying', painPoints: ['Skeptical of urgency claims'] },\n    { segmentName: 'Loyal Repeat Customer', weight: 0.5, description: 'Existing customer motivated by recognition and perks', painPoints: ['Promo fatigue from frequent emails'] }\n  ];\n}\n\n// Normalize weights so they sum to 1\nconst totalWeight = segments.reduce((sum, s) => sum + (typeof s.weight === 'number' ? s.weight : 0), 0);\nconst normalizedSegments = segments.map((s, idx) => ({\n  personaId: 'PERSONA-' + (idx + 1),\n  segmentName: s.segmentName || ('Segment ' + (idx + 1)),\n  weight: totalWeight > 0 ? Math.round((s.weight / totalWeight) * 1000) / 1000 : Math.round((1 / segments.length) * 1000) / 1000,\n  description: s.description || '',\n  painPoints: s.painPoints || []\n}));\n\nreturn {\n  json: {\n    ...item,\n    personaPanel: normalizedSegments,\n    panelSize: normalizedSegments.length\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "4137b5ca-1c78-417e-9eea-1ce5e98c07f9",
      "name": "Wait - Before Persona Reaction AI",
      "type": "n8n-nodes-base.wait",
      "position": [
        1856,
        928
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "c421866a-a6dc-41c5-bba7-b962eaddf994",
      "name": "AI - Generate Persona Panel Reactions",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2080,
        928
      ],
      "parameters": {
        "text": "=You are running a synthetic pre-launch focus group. You will role-play as each customer persona below and react to a real marketing campaign, in character, as that persona would.\n\nCampaign Name: {{ $json.campaignName }}\nChannel: {{ $json.campaignChannel }}\nBrand Voice: {{ $json.brandVoice }}\nCall to Action: {{ $json.campaignCTA }}\nOffer Details: {{ $json.offerDetails }}\n\nCampaign Copy:\n{{ $json.campaignCopy }}\n\nCustomer Personas To Simulate (JSON):\n{{ JSON.stringify($json.personaPanel) }}\n\nFor EACH persona listed above, in character, react to this exact campaign copy as that specific customer would. Stay grounded in that persona's description and pain points; do not be uniformly positive or negative across personas \u2014 reactions should differ based on who the persona actually is. Also report your own confidence that the campaign copy above was clear and substantial enough to simulate real reactions against, as opposed to being empty, garbled, or too vague.\n\nReturn a JSON object with EXACTLY these fields:\n{\n  \"panelReactionConfidence\": <number 1-10, 10 = campaign copy was clear enough to simulate confident, distinct reactions>,\n  \"reactions\": [\n    {\n      \"personaId\": \"matching the personaId above\",\n      \"segmentName\": \"matching segmentName above\",\n      \"personaReactionQuote\": \"A short first-person in-character reaction, one to two sentences, in this persona's own voice\",\n      \"sentiment\": \"Positive|Neutral|Negative|Mixed\",\n      \"purchaseIntentScore\": <number 0-10>,\n      \"clarityRating\": <number 0-10, how clear the offer was to this persona>,\n      \"keyObjections\": [\"objection1\", \"objection2\"],\n      \"standoutPhrase\": \"The specific phrase from the copy that landed best or worst for this persona\",\n      \"wouldShareWithFriend\": true or false\n    }\n  ]\n}\n\nReturn ONLY the JSON object. No markdown, no explanation.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "8827548f-623f-4955-8ef6-b5cb17eca9c4",
      "name": "Anthropic - Persona Reaction Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        2160,
        1152
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-20250514"
        },
        "options": {
          "temperature": 0.7
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "a913faa8-83fc-47b6-afc9-7e0701da7333",
      "name": "JS - Parse Persona Reactions Output",
      "type": "n8n-nodes-base.code",
      "position": [
        2352,
        928
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nlet parsed = {};\nlet reactionsParsedSuccessfully = true;\ntry {\n  const rawText = item.output || item.text || item.response || '{}';\n  const clean = rawText.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n  if (!Array.isArray(parsed.reactions) || parsed.reactions.length === 0) throw new Error('empty reactions');\n} catch (e) {\n  reactionsParsedSuccessfully = false;\n  parsed = {\n    panelReactionConfidence: 3,\n    reactions: (item.personaPanel || []).map(p => ({\n      personaId: p.personaId,\n      segmentName: p.segmentName,\n      personaReactionQuote: 'Auto-generated placeholder: could not parse AI persona reactions.',\n      sentiment: 'Neutral',\n      purchaseIntentScore: 5,\n      clarityRating: 5,\n      keyObjections: ['AI reaction parsing failed - treat as a placeholder, not a real signal'],\n      standoutPhrase: '',\n      wouldShareWithFriend: false\n    }))\n  };\n}\n\nreturn {\n  json: {\n    ...item,\n    personaReactions: parsed.reactions,\n    panelReactionConfidence: parsed.panelReactionConfidence,\n    reactionCount: parsed.reactions.length,\n    reactionsParsedSuccessfully\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "101a32fa-420c-45fd-855b-4415097eda05",
      "name": "IF - Panel Reactions Usable",
      "type": "n8n-nodes-base.if",
      "position": [
        2576,
        928
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-panel-confidence",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.panelReactionConfidence }}",
              "rightValue": 4
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f2c7e44a-1757-47a8-a203-04b50b81f7f2",
      "name": "Respond to Webhook - Campaign Input Insufficient",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2800,
        1024
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n  \"success\": false,\n  \"campaignId\": \"{{ $json.campaignId }}\",\n  \"status\": \"Campaign Input Insufficient\",\n  \"panelReactionConfidence\": {{ $json.panelReactionConfidence }},\n  \"message\": \"The campaign copy provided was too thin, vague, or garbled to simulate confident persona reactions. Please provide clearer campaign copy, offer details, and CTA, then resubmit.\"\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "d1988b83-c210-4b60-856e-02e860d9c9c5",
      "name": "JS - Calculate Aggregate Campaign Scores",
      "type": "n8n-nodes-base.code",
      "position": [
        2800,
        832
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst reactions = item.personaReactions || [];\nconst panel = item.personaPanel || [];\n\nconst weightById = {};\npanel.forEach(p => { weightById[p.personaId] = p.weight; });\n\nconst sentimentValueMap = { Positive: 100, Mixed: 50, Neutral: 40, Negative: 0 };\n\nlet weightedPurchaseIntent = 0;\nlet weightedClarity = 0;\nlet weightedSentimentScore = 0;\nlet weightedShareRate = 0;\nlet totalObjections = 0;\nlet totalWeightUsed = 0;\nconst objectionFrequency = {};\n\nreactions.forEach(r => {\n  const weight = weightById[r.personaId] || (reactions.length > 0 ? 1 / reactions.length : 0);\n  totalWeightUsed += weight;\n  weightedPurchaseIntent += (r.purchaseIntentScore || 0) * weight;\n  weightedClarity += (r.clarityRating || 0) * weight;\n  weightedSentimentScore += (sentimentValueMap[r.sentiment] !== undefined ? sentimentValueMap[r.sentiment] : 40) * weight;\n  weightedShareRate += (r.wouldShareWithFriend ? 1 : 0) * weight;\n  (r.keyObjections || []).forEach(obj => {\n    totalObjections += 1;\n    objectionFrequency[obj] = (objectionFrequency[obj] || 0) + 1;\n  });\n});\n\nif (totalWeightUsed > 0) {\n  weightedPurchaseIntent = weightedPurchaseIntent / totalWeightUsed;\n  weightedClarity = weightedClarity / totalWeightUsed;\n  weightedSentimentScore = weightedSentimentScore / totalWeightUsed;\n  weightedShareRate = weightedShareRate / totalWeightUsed;\n}\n\nconst objectionDensity = reactions.length > 0 ? Math.round((totalObjections / reactions.length) * 100) / 100 : 0;\n\nconst topObjections = Object.entries(objectionFrequency)\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 3)\n  .map(([text, count]) => text + ' (mentioned by ' + count + ')');\n\nconst compositeAppealScore = Math.max(0, Math.min(100, Math.round(\n  weightedPurchaseIntent * 10 * 0.35 +\n  weightedClarity * 10 * 0.25 +\n  weightedSentimentScore * 0.25 +\n  weightedShareRate * 100 * 0.15 -\n  objectionDensity * 5\n)));\n\nlet readinessTier = 'Launch Ready';\nif (compositeAppealScore < 35) readinessTier = 'Do Not Launch';\nelse if (compositeAppealScore < 55) readinessTier = 'Needs Revision';\nelse if (compositeAppealScore < 75) readinessTier = 'Minor Tweaks Needed';\n\nreturn {\n  json: {\n    ...item,\n    weightedPurchaseIntent: Math.round(weightedPurchaseIntent * 10) / 10,\n    weightedClarity: Math.round(weightedClarity * 10) / 10,\n    weightedSentimentScore: Math.round(weightedSentimentScore * 10) / 10,\n    weightedShareRate: Math.round(weightedShareRate * 1000) / 1000,\n    objectionDensity,\n    topObjections,\n    compositeAppealScore,\n    readinessTier\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "7e0b2738-fc89-49e9-a949-c1fe5ebc9d7f",
      "name": "Wait - Before Synthesis AI",
      "type": "n8n-nodes-base.wait",
      "position": [
        3024,
        832
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "c7f41e79-46f2-4bd8-8b03-426f160d53a9",
      "name": "AI - Generate Focus Group Synthesis & Recommendation",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        3248,
        832
      ],
      "parameters": {
        "text": "=You are a market research analyst producing a pre-launch focus-group synthesis for a marketing lead, not a final go/no-go authority on its own.\n\nCampaign Name: {{ $json.campaignName }}\nChannel: {{ $json.campaignChannel }}\nBudget At Stake: {{ $json.budgetAtStake }}\n\nComposite Appeal Score: {{ $json.compositeAppealScore }}/100 ({{ $json.readinessTier }})\nWeighted Purchase Intent: {{ $json.weightedPurchaseIntent }}/10\nWeighted Clarity: {{ $json.weightedClarity }}/10\nWeighted Sentiment Score: {{ $json.weightedSentimentScore }}/100\nObjection Density: {{ $json.objectionDensity }} objections per persona\nTop Recurring Objections: {{ JSON.stringify($json.topObjections) }}\n\nPersona Reactions (JSON, one entry per simulated customer segment):\n{{ JSON.stringify($json.personaReactions) }}\n\nGuidance: Do Not Launch or Needs Revision tiers combined with high objection density should generally lean toward Revise Before Launch or Do Not Launch. Minor Tweaks Needed tier can lean toward Launch with Minor Edits. Launch Ready tier with low objection density can lean toward Launch As-Is. Always ground your reasoning in the actual numbers and persona quotes above.\n\nReturn a JSON object with EXACTLY these fields:\n{\n  \"overallRecommendation\": \"Launch As-Is|Launch with Minor Edits|Revise Before Launch|Do Not Launch\",\n  \"executiveSummary\": \"Three to five sentence summary of how the panel reacted overall\",\n  \"topStrengths\": [\"strength1\", \"strength2\"],\n  \"topRisks\": [\"risk1\", \"risk2\"],\n  \"suggestedCopyChanges\": [\"change1\", \"change2\"],\n  \"segmentsToPrioritize\": [\"segmentName1\"],\n  \"recommendationConfidence\": <number 0-100>\n}\n\nReturn ONLY the JSON object. No markdown, no explanation.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "6e70af38-52ec-48fd-b189-3176c1ffa3b4",
      "name": "Anthropic - Synthesis Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        3328,
        1056
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-20250514"
        },
        "options": {
          "temperature": 0.3
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "62d123b7-7e82-4761-98fc-6125c63dab98",
      "name": "JS - Parse Synthesis Output & Format Report Record",
      "type": "n8n-nodes-base.code",
      "position": [
        3520,
        832
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nlet parsed = {};\ntry {\n  const rawText = item.output || item.text || item.response || '{}';\n  const clean = rawText.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n} catch (e) {\n  parsed = {\n    overallRecommendation: 'Revise Before Launch',\n    executiveSummary: 'Auto-flagged: could not parse AI synthesis output, defaulting to a cautious Revise Before Launch recommendation.',\n    topStrengths: [],\n    topRisks: ['AI synthesis parsing failed - treat this recommendation as a placeholder pending manual review'],\n    suggestedCopyChanges: [],\n    segmentsToPrioritize: [],\n    recommendationConfidence: 40\n  };\n}\n\nconst decidedAt = new Date().toISOString();\n\nreturn {\n  json: {\n    ...item,\n    ...parsed,\n    decidedAt,\n    reportSummaryRow: [\n      item.campaignId || 'N/A',\n      item.campaignName || '',\n      item.campaignChannel || '',\n      item.budgetAtStake || 0,\n      item.panelSize || 0,\n      item.compositeAppealScore || 0,\n      item.readinessTier || '',\n      item.weightedPurchaseIntent || 0,\n      item.weightedClarity || 0,\n      parsed.overallRecommendation || '',\n      JSON.stringify(parsed.topRisks || []),\n      decidedAt\n    ]\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c91c076f-2d20-4437-a60a-3619f6f8c032",
      "name": "Wait - Before Report Delivery",
      "type": "n8n-nodes-base.wait",
      "position": [
        3744,
        832
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "541d6f6d-787f-467f-a291-0ff12c12282b",
      "name": "Save Focus Group Report to Google Sheets Tracker",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3968,
        832
      ],
      "parameters": {
        "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/FocusGroupReports!A1:append?valueInputOption=USER_ENTERED",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"values\": [{{ JSON.stringify($json.reportSummaryRow) }}]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api"
      },
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "35613783-0280-4330-a981-ecc5f8cc8c49",
      "name": "IF - Launch Ready",
      "type": "n8n-nodes-base.if",
      "position": [
        4192,
        832
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "cond-launch-as-is",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.overallRecommendation }}",
              "rightValue": "Launch As-Is"
            },
            {
              "id": "cond-launch-minor-edits",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.overallRecommendation }}",
              "rightValue": "Launch with Minor Edits"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "69c9fb3a-5dab-495a-8956-e88609a7e854",
      "name": "Send Campaign Ready Notification Email",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4416,
        640
      ],
      "parameters": {
        "url": "https://api.sendgrid.com/v3/mail/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"personalizations\": [{\"to\": [{\"email\": \"growth-marketing@youragency.com\"}]}],\n  \"from\": {\"email\": \"synthetic-focus-group@youragency.com\", \"name\": \"Synthetic Focus Group\"},\n  \"subject\": \"{{ $json.overallRecommendation }}: {{ $json.campaignName }} ({{ $json.campaignId }})\",\n  \"content\": [{\n    \"type\": \"text/plain\",\n    \"value\": \"Campaign {{ $json.campaignId }} ({{ $json.campaignName }}) tested well against the synthetic panel.\\n\\nRecommendation: {{ $json.overallRecommendation }}\\nComposite Appeal Score: {{ $json.compositeAppealScore }}/100 ({{ $json.readinessTier }})\\nWeighted Purchase Intent: {{ $json.weightedPurchaseIntent }}/10 | Weighted Clarity: {{ $json.weightedClarity }}/10\\nBudget At Stake: {{ $json.budgetAtStake }}\\n\\nExecutive Summary: {{ $json.executiveSummary }}\\nTop Strengths: {{ JSON.stringify($json.topStrengths) }}\\nSegments To Prioritize: {{ JSON.stringify($json.segmentsToPrioritize) }}\"\n  }]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "83813dcb-d6f8-4251-84b7-2c6e8c718c5c",
      "name": "Respond to Webhook - Launch Ready",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        4640,
        640
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n  \"success\": true,\n  \"campaignId\": \"{{ $json.campaignId }}\",\n  \"overallRecommendation\": \"{{ $json.overallRecommendation }}\",\n  \"compositeAppealScore\": {{ $json.compositeAppealScore }},\n  \"readinessTier\": \"{{ $json.readinessTier }}\",\n  \"executiveSummary\": \"{{ $json.executiveSummary }}\",\n  \"topStrengths\": {{ JSON.stringify($json.topStrengths) }},\n  \"topRisks\": {{ JSON.stringify($json.topRisks) }},\n  \"suggestedCopyChanges\": {{ JSON.stringify($json.suggestedCopyChanges) }},\n  \"personaReactions\": {{ JSON.stringify($json.personaReactions) }},\n  \"message\": \"Focus group test complete. This campaign tested as launch-ready and the growth marketing team has been notified.\"\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "6912fab9-edca-4a41-a0c9-e8281fb297c3",
      "name": "Send Revision Required Notification Email",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4416,
        832
      ],
      "parameters": {
        "url": "https://api.sendgrid.com/v3/mail/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"personalizations\": [{\"to\": [{\"email\": \"creative-team@youragency.com\"}]}],\n  \"from\": {\"email\": \"synthetic-focus-group@youragency.com\", \"name\": \"Synthetic Focus Group\"},\n  \"subject\": \"{{ $json.overallRecommendation }}: {{ $json.campaignName }} ({{ $json.campaignId }})\",\n  \"content\": [{\n    \"type\": \"text/plain\",\n    \"value\": \"Campaign {{ $json.campaignId }} ({{ $json.campaignName }}) needs work before it goes out.\\n\\nRecommendation: {{ $json.overallRecommendation }}\\nComposite Appeal Score: {{ $json.compositeAppealScore }}/100 ({{ $json.readinessTier }})\\nObjection Density: {{ $json.objectionDensity }} objections per persona\\nTop Recurring Objections: {{ JSON.stringify($json.topObjections) }}\\n\\nExecutive Summary: {{ $json.executiveSummary }}\\nTop Risks: {{ JSON.stringify($json.topRisks) }}\\nSuggested Copy Changes: {{ JSON.stringify($json.suggestedCopyChanges) }}\"\n  }]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "047a1ff3-f001-418a-bd2b-6c540363fbdd",
      "name": "Respond to Webhook - Revision Required",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        4640,
        832
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n  \"success\": true,\n  \"campaignId\": \"{{ $json.campaignId }}\",\n  \"overallRecommendation\": \"{{ $json.overallRecommendation }}\",\n  \"compositeAppealScore\": {{ $json.compositeAppealScore }},\n  \"readinessTier\": \"{{ $json.readinessTier }}\",\n  \"executiveSummary\": \"{{ $json.executiveSummary }}\",\n  \"topRisks\": {{ JSON.stringify($json.topRisks) }},\n  \"suggestedCopyChanges\": {{ JSON.stringify($json.suggestedCopyChanges) }},\n  \"personaReactions\": {{ JSON.stringify($json.personaReactions) }},\n  \"message\": \"Focus group test complete. This campaign needs revision before launch; the creative team has been notified with suggested changes.\"\n}"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "4f17eb50-248b-47ff-82a5-c8c2b05ff21b",
  "connections": {
    "IF - Launch Ready": {
      "main": [
        [
          {
            "node": "Send Campaign Ready Notification Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Revision Required Notification Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond to Webhook - Revision Required",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Build Persona Panel": {
      "main": [
        [
          {
            "node": "Wait - Before Persona Reaction AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - Before Synthesis AI": {
      "main": [
        [
          {
            "node": "AI - Generate Focus Group Synthesis & Recommendation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic - Synthesis Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Generate Focus Group Synthesis & Recommendation",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "IF - Panel Reactions Usable": {
      "main": [
        [
          {
            "node": "JS - Calculate Aggregate Campaign Scores",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond to Webhook - Campaign Input Insufficient",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - Before Report Delivery": {
      "main": [
        [
          {
            "node": "Save Focus Group Report to Google Sheets Tracker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set - Prepare Campaign Context": {
      "main": [
        [
          {
            "node": "JS - Build Persona Panel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - Before Persona Reaction AI": {
      "main": [
        [
          {
            "node": "AI - Generate Persona Panel Reactions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic - Persona Reaction Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Generate Persona Panel Reactions",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "JS - Parse Persona Reactions Output": {
      "main": [
        [
          {
            "node": "IF - Panel Reactions Usable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Generate Persona Panel Reactions": {
      "main": [
        [
          {
            "node": "JS - Parse Persona Reactions Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Receive Campaign Submission": {
      "main": [
        [
          {
            "node": "Set - Prepare Campaign Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Campaign Ready Notification Email": {
      "main": [
        [
          {
            "node": "Respond to Webhook - Launch Ready",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Calculate Aggregate Campaign Scores": {
      "main": [
        [
          {
            "node": "Wait - Before Synthesis AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Focus Group Report to Google Sheets Tracker": {
      "main": [
        [
          {
            "node": "IF - Launch Ready",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Parse Synthesis Output & Format Report Record": {
      "main": [
        [
          {
            "node": "Wait - Before Report Delivery",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Generate Focus Group Synthesis & Recommendation": {
      "main": [
        [
          {
            "node": "JS - Parse Synthesis Output & Format Report Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}