AutomationFlowsAI & RAG › Generate and Vet Ad Creative with Openai, Spendict, Google Sheets, and Slack

Generate and Vet Ad Creative with Openai, Spendict, Google Sheets, and Slack

BySpendict @spendict on n8n.io

This workflow collects a product brief via an n8n form, uses OpenAI to generate ad variants, gates each one with Spendict run/fix/kill scoring, auto-rewrites fixable ads, logs approved results to Google Sheets, and posts an approval count to Slack. Receives a product brief from…

Event trigger★★★★☆ complexity21 nodesForm TriggerHTTP RequestGoogle Sheets
AI & RAG Trigger: Event Nodes: 21 Complexity: ★★★★☆ Added:

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

This workflow follows the Form Trigger → Google Sheets recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "LofomClskBHHb2aB",
  "name": "AI ad factory with a Spendict quality gate (generate \u2192 gate \u2192 self-heal \u2192 log \u2192 notify)",
  "tags": [],
  "nodes": [
    {
      "id": "6fddee74-85ac-4f81-8f92-103086387b8d",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "color": 4,
        "width": 520,
        "height": 680,
        "content": "# \ud83c\udfed AI ad factory, gated by Spendict\n\nGenerate ad variants with AI, let **Spendict** score each one, auto-fix the weak ones, and only log the winners \u2014 so your agent never funds a bad ad.\n\n## \u2699\ufe0f Setup\n1. **OpenAI** \u2014 Header Auth `Authorization: Bearer sk-\u2026` on both OpenAI nodes.\n2. **Spendict** \u2014 free key at spendict.com/dashboard \u2192 Header Auth `Authorization: Bearer spd_live_\u2026` on both Spendict nodes.\n3. **Google Sheets** \u2014 connect Google; pick a sheet with header columns: `status, headline, primary_text, platform, verdict, score, top_fix`.\n4. **Slack** \u2014 paste your Incoming Webhook URL into the **Slack: post summary** node.\n\n## \u25b6\ufe0f Run\nClick **Test workflow**, fill the form (product, platform, audience, count), and go. Swap the Form trigger for a Webhook / Airtable / Shopify trigger to run it on autopilot."
      },
      "typeVersion": 1
    },
    {
      "id": "afd48032-77ba-4e8a-9553-e9258a6ef231",
      "name": "Section: generate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        288
      ],
      "parameters": {
        "color": 5,
        "width": 620,
        "height": 150,
        "content": "### \u2460 Generate variants\nA form brief \u2192 **OpenAI** writes N distinct ad variants (headline + primary text) \u2192 we split them into one item per variant."
      },
      "typeVersion": 1
    },
    {
      "id": "010be9a1-b56b-4369-8131-2e3d4a4df9c2",
      "name": "Section: gate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1392,
        288
      ],
      "parameters": {
        "color": 3,
        "width": 460,
        "height": 150,
        "content": "### \u2461 Gate with Spendict\n**Spendict** scores each variant and returns a deterministic **run / fix_first / kill**. The Switch routes each ad by its verdict \u2014 the model can't green-light its own bad ad."
      },
      "typeVersion": 1
    },
    {
      "id": "164c7038-3f43-45d0-88f3-27c1a79e0fe5",
      "name": "Section: self-heal",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        1024
      ],
      "parameters": {
        "color": 6,
        "width": 1100,
        "height": 150,
        "content": "### \u2462 Self-heal the fixable ones\n`fix_first` variants aren't discarded \u2014 the prioritized fixes are fed back to **OpenAI**, the ad is rewritten, and **Spendict re-scores** it. An automatic second chance."
      },
      "typeVersion": 1
    },
    {
      "id": "28f777a1-a6fc-4f52-996b-a411a92b8433",
      "name": "Section: log & notify",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 860,
        "height": 150,
        "content": "### \u2463 Log winners & notify\nApproved ads (clean `run` + auto-fixed) are appended to **Google Sheets** and a summary is posted to **Slack**. Your shippable ads, in one place.\n\n_Sheets + Slack are optional \u2014 delete them and the core still runs._"
      },
      "typeVersion": 1
    },
    {
      "id": "f0389346-2706-4c49-aa66-8f7c62a05f5a",
      "name": "Section: killed",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1136,
        1008
      ],
      "parameters": {
        "color": 2,
        "width": 380,
        "height": 150,
        "content": "### \u270b Killed ads\n`kill` verdicts are logged separately and never spent on \u2014 the whole point."
      },
      "typeVersion": 1
    },
    {
      "id": "2d0f9713-bc51-43c9-80a6-2c0ce88c53b7",
      "name": "Product brief",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        672,
        480
      ],
      "parameters": {
        "options": {},
        "formTitle": "Generate + gate ad variants",
        "formFields": {
          "values": [
            {
              "fieldLabel": "product",
              "placeholder": "e.g. Spendict \u2014 deterministic run/fix/kill verdicts for AI ad creative",
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "platform",
              "fieldOptions": {
                "values": [
                  {
                    "option": "meta"
                  },
                  {
                    "option": "tiktok"
                  },
                  {
                    "option": "google"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldLabel": "audience",
              "placeholder": "e.g. DTC founders running AI ad agents"
            },
            {
              "fieldType": "number",
              "fieldLabel": "count",
              "placeholder": "5"
            }
          ]
        },
        "formDescription": "Describe the offer. We'll generate ad variants and only surface the ones worth spending on."
      },
      "typeVersion": 2.2
    },
    {
      "id": "55d1e2e5-f49e-4256-a5fc-64949cfe424e",
      "name": "OpenAI: generate variants",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        480
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4o-mini', response_format: { type: 'json_object' }, messages: [ { role: 'system', content: 'You are a senior direct-response performance marketer. Write scroll-stopping, specific ad copy that leads with a concrete pain or desire. Return ONLY JSON.' }, { role: 'user', content: 'Write ' + ($json.count || 5) + ' distinct ' + $json.platform + ' ad variants. Product/offer: ' + $json.product + '. Target audience: ' + ($json.audience || 'general') + '. Each variant needs a punchy headline and a 1-2 sentence primary_text. Return {\"variants\":[{\"headline\":\"...\",\"primary_text\":\"...\"}]}' } ] }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "255e76aa-53d5-4a3e-a883-e3d13642d8e6",
      "name": "Split into variants",
      "type": "n8n-nodes-base.code",
      "position": [
        1072,
        480
      ],
      "parameters": {
        "jsCode": "const brief = $('Product brief').first().json;\nlet variants = [];\ntry {\n  variants = JSON.parse($json.choices[0].message.content).variants || [];\n} catch (e) {\n  throw new Error('Could not parse OpenAI response as JSON');\n}\nreturn variants.map((v) => ({\n  json: {\n    headline: v.headline || '',\n    primary_text: v.primary_text || v.body || '',\n    platform: brief.platform || 'meta',\n    product_context: brief.product || '',\n    target_audience: brief.audience || ''\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "da12129d-db6d-475b-9c82-c1d6b916db86",
      "name": "Spendict: assess_ad_creative",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1280,
        480
      ],
      "parameters": {
        "url": "https://www.spendict.com/api/v1/assess",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ ad_copy: { headline: $json.headline, primary_text: $json.primary_text }, platform: $json.platform, product_context: $json.product_context, target_audience: $json.target_audience }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "8009044c-d71e-44f0-84e6-526be8a40e52",
      "name": "Gate on verdict",
      "type": "n8n-nodes-base.switch",
      "position": [
        1488,
        480
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "run",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.launch_recommendation }}",
                    "rightValue": "run"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "fix_first",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.launch_recommendation }}",
                    "rightValue": "fix_first"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "kill"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "6a5624ff-febb-4415-ab5d-81d4315f81d3",
      "name": "Row: approved",
      "type": "n8n-nodes-base.code",
      "position": [
        1712,
        480
      ],
      "parameters": {
        "jsCode": "const ad = $('Split into variants').item.json;\nreturn [{ json: {\n  status: 'approved',\n  headline: ad.headline,\n  primary_text: ad.primary_text,\n  platform: ad.platform,\n  verdict: $json.launch_recommendation,\n  score: $json.overall_score,\n  top_fix: ''\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "948b5358-3a8e-47c3-bbc3-49045bc6cedf",
      "name": "OpenAI: rewrite with fixes",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1712,
        832
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4o-mini', response_format: { type: 'json_object' }, messages: [ { role: 'system', content: 'You are a senior direct-response performance marketer. Rewrite the ad to fix the listed problems, keeping it specific and scroll-stopping. Return ONLY JSON.' }, { role: 'user', content: 'Original headline: ' + $('Split into variants').item.json.headline + '. Original body: ' + $('Split into variants').item.json.primary_text + '. Fixes to apply: ' + JSON.stringify($json.prioritized_fixes) + '. Return {\"headline\":\"...\",\"primary_text\":\"...\"}' } ] }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "f0f3e195-bd58-480c-93de-59e333032362",
      "name": "Parse rewritten ad",
      "type": "n8n-nodes-base.code",
      "position": [
        1952,
        832
      ],
      "parameters": {
        "jsCode": "const orig = $('Split into variants').item.json;\nlet fixed = {};\ntry { fixed = JSON.parse($json.choices[0].message.content); } catch (e) { fixed = {}; }\nreturn [{ json: {\n  headline: fixed.headline || orig.headline,\n  primary_text: fixed.primary_text || orig.primary_text,\n  platform: orig.platform,\n  product_context: orig.product_context,\n  target_audience: orig.target_audience\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "57cc4d83-ec4c-4585-b232-460fc2c6ee56",
      "name": "Spendict: re-assess",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2208,
        832
      ],
      "parameters": {
        "url": "https://www.spendict.com/api/v1/assess",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ ad_copy: { headline: $json.headline, primary_text: $json.primary_text }, platform: $json.platform, product_context: $json.product_context, target_audience: $json.target_audience }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "e8a50e2c-b149-48ff-b6d2-bb7be0fbca0d",
      "name": "Row: auto-fixed",
      "type": "n8n-nodes-base.code",
      "position": [
        2464,
        832
      ],
      "parameters": {
        "jsCode": "const ad = $('Parse rewritten ad').item.json;\nreturn [{ json: {\n  status: 'auto-fixed',\n  headline: ad.headline,\n  primary_text: ad.primary_text,\n  platform: ad.platform,\n  verdict: $json.launch_recommendation,\n  score: $json.overall_score,\n  top_fix: ''\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "a1fa7d0e-c5f0-40cf-b639-3a3a91a31c7c",
      "name": "Collect winners",
      "type": "n8n-nodes-base.merge",
      "position": [
        2784,
        480
      ],
      "parameters": {},
      "typeVersion": 3
    },
    {
      "id": "7e9b32d0-a128-455d-9af8-cc6d6c869795",
      "name": "Google Sheets: log approved",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2992,
        480
      ],
      "parameters": {
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "3c263a6d-c9f1-48e6-9972-249f5f639bea",
      "name": "Approved ads",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        3184,
        480
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "f1a01b1c-25db-4c43-9614-37e18b7c46e5",
      "name": "Slack: post summary",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3392,
        480
      ],
      "parameters": {
        "url": "https://hooks.slack.com/services/REPLACE_WITH_YOUR_SLACK_WEBHOOK",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ text: '\u2705 ' + ($json.data ? $json.data.length : 0) + ' ad(s) cleared Spendict and were logged to the sheet.' }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "d96622b9-d940-4da1-b668-b34a8e462f10",
      "name": "Killed ads (logged)",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1264,
        832
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "b762225e-b802-47a8-bb19-4b31ae596c20",
  "nodeGroups": [],
  "connections": {
    "Approved ads": {
      "main": [
        [
          {
            "node": "Slack: post summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Product brief": {
      "main": [
        [
          {
            "node": "OpenAI: generate variants",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Row: approved": {
      "main": [
        [
          {
            "node": "Collect winners",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect winners": {
      "main": [
        [
          {
            "node": "Google Sheets: log approved",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gate on verdict": {
      "main": [
        [
          {
            "node": "Row: approved",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "OpenAI: rewrite with fixes",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Killed ads (logged)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Row: auto-fixed": {
      "main": [
        [
          {
            "node": "Collect winners",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Parse rewritten ad": {
      "main": [
        [
          {
            "node": "Spendict: re-assess",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Spendict: re-assess": {
      "main": [
        [
          {
            "node": "Row: auto-fixed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split into variants": {
      "main": [
        [
          {
            "node": "Spendict: assess_ad_creative",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI: generate variants": {
      "main": [
        [
          {
            "node": "Split into variants",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI: rewrite with fixes": {
      "main": [
        [
          {
            "node": "Parse rewritten ad",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets: log approved": {
      "main": [
        [
          {
            "node": "Approved ads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Spendict: assess_ad_creative": {
      "main": [
        [
          {
            "node": "Gate on verdict",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow collects a product brief via an n8n form, uses OpenAI to generate ad variants, gates each one with Spendict run/fix/kill scoring, auto-rewrites fixable ads, logs approved results to Google Sheets, and posts an approval count to Slack. Receives a product brief from…

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This workflow collects a campaign brief via an n8n form, uses Spendict to generate a targeting strategy, uses OpenAI to draft platform-specific ad variants, then has Spendict score and gate each draft

Form Trigger, HTTP Request, Google Sheets
AI & RAG

Generate realistic, high-quality images from text prompts using the Flux AI Text-to-Image Generator API via RapidAPI, and seamlessly store the results in Google Drive and log them in Google Sheets — a

Form Trigger, HTTP Request, Google Sheets +1
AI & RAG

This workflow collects an ad campaign build brief via an n8n form, audits the structure with Spendict, and automatically rewrites failing builds with OpenAI before re-auditing, then logs the outcome t

Form Trigger, HTTP Request, Google Sheets
AI & RAG

This workflow hosts an n8n intake form, sends submissions to Anthropic Claude for practice-area classification and qualification, then routes results to Gmail and Google Sheets before returning a conf

Form Trigger, HTTP Request, Gmail +2
AI & RAG

This workflow collects SIP and mutual fund details via an n8n Form, calculates goal and performance metrics, generates a personalized advisory using OpenAI, emails a color-coded HTML report via Gmail,

Form Trigger, HTTP Request, Google Sheets +1