Back to n8n Workflows

Fan Out Creative Testing

Mike Taylor Mike Taylor

Automatically generate 8 ad creative variations from your original concept, then test all 9 versions with AI personas to predict CTR performance. Get instant creative testing results without spending a penny on real ads.

Fan Out Creative Testing n8n workflow diagram

Click to expand

Summarize in
OR
View full transcript

 All right, let me walk you through this N eight N template. We are doing something quite simple, which is we take some brand that you're writing ad copy for. You write in a headline and then you write the description you submit. So this form is created by NN

This is a nice headline. So whatever ad copy you wanna put in here, this is just some copy line that I used to use for ladder. Just submit that and then it comes into the output here. And then we run it through an LLM. What I'm asking it to do is to come up with eight new very creative variations on this Google ad by changing the headline description or both giving it the existing headline description, which is dragged in from here.

And you can see that's what it looks like filled in. And then information about the brand. And then I've given it some examples of types of. A copy that you can have. So when you execute the step, it runs it. This is specifically with the Google model, and you can see that here, Google Gemini Chat.

I've added a model to the chain and I've added a structured output passer. And yeah, you can see that it output the schema in the right format. It started with the the one that I entered and then it came up with. A bunch of new experiments here.

So we test you grow. That's pretty cool actually. 10,000 tests. Your growth, certainty in growth. Skip the guesswork, grow.

 All right we're gonna see which one performs

 best. So we're gonna execute this step with a rally. We have an API key and then just sending 

 to my, to that URL with these parameters.

And this is specific to rally, but using the Google model. And then sending in our prompt here. So we've given it all of the different options and we're just asking it, which one would you click on? And that's gonna run that through a hundred different AI personas. And we have the votes, like the options here.

So we have this step which pulls out for the individual personas, what are the different responses. So it just pulls out the responses. And then we have some code to calculate. The score which just basically takes each of the headlines, descriptions and which option it was and then counts. As a percentage what the CTR was, so how many AI agents clicked on each one.

And then I have just a really basic thing that creates a HTML report. And you can see grow without the guesswork that actually worked best. 59% click clear eight but we had these other options which performed as well. And sometimes we actually managed to be the original, which is quite cool.

But the great thing about this is that. So fully automated, you could hit this as an endpoint with every ad that you create and you could just see if you can beat it using ai. And and then you're using ai to to check whether the performance is good as well. Because you have these ai, this AI focus group that of a hundred people who are based on your customers and they're trying to check which of these things would they click on.

And that, that makes a really big difference to performance. If you do that consistently across every ad in your account you can catch some that. Not likely to perform. And then you can also identify some other winners that maybe you hadn't thought of proving growth strategies or, we test you grow like this.

Didn't do that well in the test, but I actually quite like that. And maybe I could, I. Try it with a different description or something. Cool. All right, so hopefully that shows you a little bit about this workflow. I call it the fan out creative strategy because you take one idea and then fan it out to eight different things and then condense it back down to the one that you're gonna run.

Overview

This workflow revolutionizes ad creative testing by automatically generating multiple ad variations and instantly testing them with AI personas to predict performance. Starting with a single headline and description, it uses Google Gemini to create 8 creative variations that maintain the core message while exploring different angles. These variations are then tested against Rally's AI audience using voting mode to simulate real user choice behavior, with results ranked by predicted click-through rates. Perfect for marketers who need to rapidly test creative concepts before committing to ad spend.

🎯 Pro Tips & Secret Sauce

The magic lies in the dual AI system for creative generation and performance prediction:

  1. Intelligent Creative Expansion - Google Gemini doesn't just rewrite ads randomly; it understands the core value proposition and explores different creative angles while maintaining message integrity and respecting character limits
  2. Structured Output Validation - Uses JSON schema validation to ensure all 9 variations (original + 8 new) are properly formatted with exactly 30-character headlines and 90-character descriptions
  3. Simulated Choice Architecture - Rally's voting mode presents all variations as a realistic Google Ads scenario ("Which would you click?") rather than rating each individually, mimicking real user behavior
  4. Automatic Performance Ranking - CTR calculation and sorting reveals which creative approaches resonate most with the target audience, eliminating guesswork about which variations to test first
  5. Zero-Cost Creative Testing - Get comprehensive creative performance insights before spending any ad budget, dramatically reducing the risk of poor-performing creative concepts

This creates a complete creative development and testing pipeline that would typically require weeks of work and thousands in ad spend, compressed into minutes of automated analysis.

📝 Step-by-Step Instructions

  1. Form Submission - User enters brand description, original headline, and description through the web form interface
  2. Generate Creative Variations - Google Gemini analyzes the brand context and original ad to create 8 new creative variations that explore different angles while maintaining the core message
  3. Structure Output Validation - JSON schema parser ensures all 9 variations (original + 8 new) meet Google Ads requirements with proper headline and description length limits
  4. Rally Audience Testing - Sends all 9 ad variations to Rally's AI personas in voting mode, presenting them as realistic Google search ads asking "Which would you click?"
  5. Extract Individual Votes - Splits Rally's response array to process each persona's individual voting decision for detailed analysis
  6. Calculate Click-Through Rates - Custom code processes all votes, counts selections for each variation, calculates percentages, and marries results with original ad content
  7. Display Performance Report - Formats results into a ranked HTML report showing each variation's headline, description, and predicted CTR sorted by performance from highest to lowest

📋 Requirements

Required Integrations

  • Google Gemini - AI language model for generating creative ad variations with structured output
  • Rally API - AI persona testing service for simulating real user ad selection behavior
  • Form Trigger - Web interface for inputting brand information and original ad creative
  • LangChain - Framework for chaining LLM operations with structured output parsing (n8n native)

Required Credentials

  • Google Gemini (PaLM) API credentials for creative generation
  • Rally API Bearer token with voting mode access
  • n8n instance with LangChain nodes enabled

Setup Prerequisites

  • Active Rally account with configured audience personas (default: rb842b547c27640)
  • Google AI/Gemini API access with sufficient quotas
  • n8n instance with LangChain integration capabilities
  • No external storage or additional services required

🚀 n8n Workflow Template

{
  "active": false,
  "connections": {
    "Ask Rally": {
      "main": [
        [
          {
            "index": 0,
            "node": "Get Individual Votes",
            "type": "main"
          }
        ]
      ]
    },
    "Basic LLM Chain": {
      "main": [
        [
          {
            "index": 0,
            "node": "Ask Rally",
            "type": "main"
          }
        ]
      ]
    },
    "Calculate CTR": {
      "main": [
        [
          {
            "index": 0,
            "node": "Display Report",
            "type": "main"
          }
        ]
      ]
    },
    "Get Individual Votes": {
      "main": [
        [
          {
            "index": 0,
            "node": "Calculate CTR",
            "type": "main"
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "index": 0,
            "node": "Basic LLM Chain",
            "type": "ai_languageModel"
          }
        ]
      ]
    },
    "On form submission": {
      "main": [
        [
          {
            "index": 0,
            "node": "Basic LLM Chain",
            "type": "main"
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "index": 0,
            "node": "Basic LLM Chain",
            "type": "ai_outputParser"
          }
        ]
      ]
    }
  },
  "id": "Tl8197fxk9xDTBIK",
  "meta": {
    "instanceId": "7aa2e96d57ff40383569724f8ecb13d674a87bf09d39aa5d8fa5ba31f7a8407a",
    "templateCredsSetupCompleted": true
  },
  "name": "Fan Out Creative Testing",
  "nodes": [
    {
      "id": "c6fb516a-aa31-4d5b-9c9c-9f308a5b79c0",
      "name": "On form submission",
      "parameters": {
        "formDescription": "Enter your creative idea",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Brand",
              "fieldType": "textarea",
              "placeholder": "Ladder is a growth marketing agency that has run 10,000 experiments across all major growth channels. We test and learn what works so you don\u0027t have to. We have offices in New York, London, and across the EU. We have worked with clients such as Monzo Bank, Booking.com, and Time Out Magazine."
            },
            {
              "fieldLabel": "Headline",
              "placeholder": "Growth without the guesswork",
              "requiredField": true
            },
            {
              "fieldLabel": "Description",
              "placeholder": "We\u0027ve run 10,000 experiments to learn what works so you don\u0027t have to ",
              "requiredField": true
            }
          ]
        },
        "formTitle": "Ask anything",
        "options": {},
        "responseMode": "lastNode"
      },
      "position": [
        -480,
        -60
      ],
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.2,
      "webhookId": "6c20a59e-075a-4070-850e-95353cccabf9"
    },
    {
      "credentials": {
        "httpBearerAuth": {
          "id": "HeUQ9K9oUtQ5TyHr",
          "name": "Bearer Auth account"
        }
      },
      "id": "1be487f1-f971-4ee9-b199-16aa4d39ab35",
      "name": "Ask Rally",
      "parameters": {
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "smart",
              "value": "false"
            },
            {
              "name": "provider",
              "value": "google"
            },
            {
              "name": "query",
              "value": "=You\u0027re searching on Google and you see the following ads. Which would you click?:\n\n{{ $json.output.map((ad, index) =\u003e `${String.fromCharCode(97 + index)}) ${ad.Headline}: ${ad.Description}`).join(\u0027\\n\\n\u0027) }}"
            },
            {
              "name": "audience_id",
              "value": "rb842b547c27640"
            },
            {
              "name": "voting_mode",
              "value": "true"
            }
          ]
        },
        "genericAuthType": "httpBearerAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "accept",
              "value": "application/json"
            }
          ]
        },
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "url": ""
      },
      "position": [
        340,
        -60
      ],
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2
    },
    {
      "id": "7e2a7f01-31bb-4ac1-975c-494337172639",
      "name": "Basic LLM Chain",
      "parameters": {
        "batching": {},
        "hasOutputParser": true,
        "promptType": "define",
        "text": "=Come up with 8 new very creative variations on this Google Ad by changing the Headline, Description or both. Do not deviate from the core creative idea, just express it in more creative ways. Headlines in text ads can be up to 30 characters each, while descriptions can be 90 characters each. Only respond with an array of Headline and Description combinations. Always start with the original Headline and Description you were given as the first in the list, then add your 8 new variations after.\n\n## Headline and Description\nHeadline: {{ $json.Headline }}\nDescription: {{ $json.Description }}\n\n## Brand\nHere is more information about the brand, you can use any of this so long as the core creative idea remains the same:\n{{ $json.Brand }}\n\n## Examples\nHere are some examples to show you what the task looks like when done well. You should not use these examples in your output, always make new variations relevant to the brand. \n\n**Original Ad:**\n- Headline: \"Fresh Coffee Daily\"\n- Description: \"Artisan roasted beans from around the world. Visit our cozy downtown location today!\"\n- Brand: \"Bean \u0026 Brew Coffee House - A family-owned coffee shop serving the community since 1985. Known for our warm atmosphere, friendly baristas, and commitment to fair trade coffee.\"\n\n**Output:**\n\n```json\n[\n  {\n    \"Headline\": \"Fresh Coffee Daily\",\n    \"Description\": \"Artisan roasted beans from around the world. Visit our cozy downtown location today!\"\n  },\n  {\n    \"Headline\": \"Artisan Coffee Hub\", \n    \"Description\": \"From bean to cup perfection. Join our coffee family at the heart of downtown!\"\n  },\n  {\n    \"Headline\": \"Your Daily Brew Fix\",\n    \"Description\": \"Global beans, local love. Discover your new favorite spot in downtown today!\"\n  },\n  {\n    \"Headline\": \"Coffee Craft Masters\",\n    \"Description\": \"Handcrafted perfection in every cup. Visit our warm, welcoming coffee sanctuary!\"\n  },\n  {\n    \"Headline\": \"Bean Perfection Daily\",\n    \"Description\": \"Fair trade excellence meets downtown charm. Taste the difference quality makes!\"\n  },\n  {\n    \"Headline\": \"Roasted Fresh Today\",\n    \"Description\": \"Today\u0027s fresh roast awaits you. Step into our cozy community coffee corner!\"\n  },\n  {\n    \"Headline\": \"Coffee Art \u0026 Heart\",\n    \"Description\": \"Where passion meets the perfect cup. Experience our family tradition downtown!\"\n  },\n  {\n    \"Headline\": \"Premium Beans Daily\",\n    \"Description\": \"Exceptional beans, exceptional experience. Your new downtown coffee home awaits!\"\n  }\n]\n```"
      },
      "position": [
        -160,
        -60
      ],
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7
    },
    {
      "credentials": {
        "googlePalmApi": {
          "id": "cWM5zhRjuahlwVKK",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "id": "a4676bde-62da-4917-844a-3a9c5d81ab3f",
      "name": "Google Gemini Chat Model",
      "parameters": {
        "modelName": "models/gemini-2.5-flash",
        "options": {}
      },
      "position": [
        -180,
        160
      ],
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "typeVersion": 1
    },
    {
      "id": "1dc2e11f-19a5-4236-b336-32e71697fd2b",
      "name": "Structured Output Parser",
      "parameters": {
        "inputSchema": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"array\",\n  \"title\": \"Google Ad Variations\",\n  \"description\": \"An array of creative Google Ad headline and description combinations\",\n  \"items\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"Headline\": {\n        \"type\": \"string\",\n        \"maxLength\": 30,\n        \"description\": \"The ad headline, maximum 30 characters\"\n      },\n      \"Description\": {\n        \"type\": \"string\",\n        \"maxLength\": 90,\n        \"description\": \"The ad description, maximum 90 characters\"\n      }\n    },\n    \"required\": [\"Headline\", \"Description\"],\n    \"additionalProperties\": false\n  },\n  \"minItems\": 9,\n  \"maxItems\": 9,\n  \"uniqueItems\": true\n}",
        "schemaType": "manual"
      },
      "position": [
        40,
        160
      ],
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3
    },
    {
      "id": "43b5a382-2d7e-40ec-95fc-1a67335f4044",
      "name": "Sticky Note",
      "parameters": {
        "color": 5,
        "content": "## Synthetic Testing\nTake 8 new variations and test them against the original.",
        "height": 500,
        "width": 540
      },
      "position": [
        260,
        -160
      ],
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1
    },
    {
      "id": "1df2417b-1f17-483f-a402-b593ee1db835",
      "name": "Sticky Note1",
      "parameters": {
        "content": "## Fan Out Creative\nGenerate 8 new creative variations based on the original.",
        "height": 500,
        "width": 540
      },
      "position": [
        -300,
        -160
      ],
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1
    },
    {
      "id": "40925b49-241d-442b-806e-04a5a4323953",
      "name": "Get Individual Votes",
      "parameters": {
        "fieldToSplitOut": "responses",
        "options": {}
      },
      "position": [
        560,
        -60
      ],
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1
    },
    {
      "id": "a8b1cb15-1d95-4f83-9855-526e110302de",
      "name": "Calculate CTR",
      "parameters": {
        "jsCode": "// Count occurrences of each option\nlet optionCounts = {};\nlet total = 0;\n\nfor (const item of $input.all()) {\n    try {\n        // Parse the response JSON\n        const responseData = JSON.parse(item.json.response);\n        \n        // Get the option value\n        const option = responseData.option;\n        \n        if (option) {\n            total++;\n            // Initialize count for this option if it doesn\u0027t exist\n            if (!optionCounts[option]) {\n                optionCounts[option] = 0;\n            }\n            optionCounts[option]++;\n        }\n    } catch (error) {\n        // Handle cases where response is not valid JSON\n        console.log(`Error parsing response for item ${item.json.persona_id}:`, error.message);\n    }\n}\n\n// Get ad variations using n8n syntax\nlet adVariations = [];\ntry {\n    adVariations = $(\u0027Basic LLM Chain\u0027).first().json.output;\n} catch (error) {\n    console.log(\"Error getting ad variations:\", error.message);\n}\n\n// Marry up options with ad variations and calculate percentages\nconst variationsWithAds = [];\n\nfor (const [option, count] of Object.entries(optionCounts)) {\n    const percentage = total \u003e 0 ? (count / total * 100).toFixed(1) : 0;\n    \n    // Convert option letter to index (a=0, b=1, c=2, etc.)\n    const optionIndex = option.charCodeAt(0) - 97; // \u0027a\u0027.charCodeAt(0) = 97\n    \n    // Get the corresponding ad variation\n    const adVariation = adVariations[optionIndex] || { Headline: \"Unknown\", Description: \"Unknown\" };\n    \n    variationsWithAds.push({\n        option: option,\n        count: count,\n        percentage: parseFloat(percentage),\n        headline: adVariation.Headline,\n        description: adVariation.Description\n    });\n    \n    console.log(`Option \u0027${option}\u0027: ${adVariation.Headline} - ${count} clicks (${percentage}%)`);\n}\n\n// Sort by CTR percentage descending\nvariationsWithAds.sort((a, b) =\u003e b.percentage - a.percentage);\n\nconsole.log(`\\nTotal responses: ${total}`);\nconsole.log(`CTR by variation (sorted by performance):`);\nvariationsWithAds.forEach(v =\u003e {\n    console.log(`  ${v.option.toUpperCase()}) ${v.headline}: ${v.description} | CTR: ${v.percentage}%`);\n});\n\n// Return comprehensive results\nreturn {\n    total: total,\n    variationsWithAds: variationsWithAds\n};"
      },
      "position": [
        340,
        180
      ],
      "type": "n8n-nodes-base.code",
      "typeVersion": 2
    },
    {
      "id": "2194d5a2-caf2-40a7-a5c5-bc97dc6e01fe",
      "name": "Display Report",
      "parameters": {
        "operation": "completion",
        "respondWith": "showText",
        "responseText": "=\u003ch1\u003e{{ $json.title || \u0027Ad Variation CTR Results\u0027 }}\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eTotal Responses:\u003c/strong\u003e {{ $json.total }}\u003c/p\u003e\n\u003cbr\u003e\n\u003cp\u003e-----\u003c/p\u003e\n\u003chr\u003e\n\u003cbr\u003e\n\u003c!-- each variation with ad content, sorted by CTR descending --\u003e\n{{ $json.variationsWithAds\n     .map(v =\u003e `\u0026bull;\u0026nbsp;\u003cstrong\u003e${v.option}) ${v.headline}:\u003c/strong\u003e ${v.description} \u003cbr\u003e \u003cstrong\u003eCTR: ${v.percentage}%\u003c/strong\u003e\u003cbr\u003e\u003cbr\u003e`)\n     .join(\u0027\u0027) }}"
      },
      "position": [
        560,
        180
      ],
      "type": "n8n-nodes-base.form",
      "typeVersion": 1,
      "webhookId": "205dca97-541a-4635-9957-4644c6400fa2"
    }
  ],
  "pinData": {
    "On form submission": [
      {
        "json": {
          "Brand": "Ladder is a growth marketing agency that has run 10,000 experiments across all major growth channels. We test and learn what works so you don\u0027t have to. We have offices in New York, London, and across the EU. We have worked with clients such as Monzo Bank, Booking.com, and Time Out Magazine.",
          "Description": "We\u0027ve run 10,000 experiments to test and learn what works so you don\u0027t have to.",
          "Headline": "Growth without the guesswork",
          "formMode": "test",
          "submittedAt": "2025-06-21T07:50:30.672-04:00"
        }
      }
    ]
  },
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [],
  "versionId": "dab232cf-99e1-419c-a2ad-33562adc0db6"
}

About the Author

Mike Taylor

Mike Taylor

Mike Taylor is the CEO & Co-Founder of Rally. He previously co-founded a 50-person growth marketing agency called Ladder, created marketing & AI courses on LinkedIn, Vexpower, and Udemy taken by over 450,000 people, and published a book with O’Reilly on prompt engineering.