This workflow corresponds to n8n.io template #17374 — 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 →
{
"id": "c0JXZ6cLdG80OIIZ",
"name": "Plan campaign targeting and draft pre-vetted ad variants with Spendict and OpenAI",
"tags": [],
"nodes": [
{
"id": "2693c386-0c01-483d-9862-72f96371e9a7",
"name": "Campaign brief",
"type": "n8n-nodes-base.formTrigger",
"position": [
752,
448
],
"parameters": {
"options": {},
"formTitle": "Campaign brief",
"formFields": {
"values": [
{
"fieldLabel": "product",
"placeholder": "e.g. Home fitness app, $29/mo, cancel anytime",
"requiredField": true
},
{
"fieldType": "dropdown",
"fieldLabel": "platform",
"fieldOptions": {
"values": [
{
"option": "meta"
},
{
"option": "tiktok"
},
{
"option": "google"
}
]
},
"requiredField": true
},
{
"fieldLabel": "budget",
"placeholder": "e.g. $1,500/month",
"requiredField": true
},
{
"fieldLabel": "objective",
"placeholder": "e.g. purchases, leads, app installs",
"requiredField": true
},
{
"fieldLabel": "funnel_stage",
"placeholder": "prospecting / retargeting / retention (optional)"
},
{
"fieldLabel": "vertical",
"placeholder": "e.g. DTC fitness (optional)"
}
]
},
"formDescription": "Describe the campaign. Spendict designs the targeting strategy, OpenAI drafts ads that follow it, and Spendict vets every draft."
},
"typeVersion": 2.2
},
{
"id": "1756193d-925a-425b-91f6-c9132e1de14e",
"name": "Spendict: strategize_targeting",
"type": "n8n-nodes-base.httpRequest",
"position": [
976,
448
],
"parameters": {
"url": "https://www.spendict.com/api/v1/strategize-targeting",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify({ platform: $json.platform, product: $json.product, budget: $json.budget, objective: $json.objective, funnel_stage: $json.funnel_stage || undefined, vertical: $json.vertical || undefined }) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"typeVersion": 4.2
},
{
"id": "f064aec2-9bca-44f4-bb3f-6fa21ee33518",
"name": "Format the plan",
"type": "n8n-nodes-base.code",
"position": [
1216,
448
],
"parameters": {
"jsCode": "const s = $json;\nconst lines = [];\nlines.push('\ud83c\udfaf *Targeting strategy \u2014 ' + (s.platform_identified || 'campaign') + '* (confidence: ' + (s.confidence || 'n/a') + ')');\nlines.push('');\nlines.push('*Summary:* ' + s.strategy_summary);\nlines.push('*Audience approach:* ' + s.audience_approach);\nlines.push('');\nlines.push('*Segments:*');\nfor (const seg of s.segmentation || []) lines.push(' \u2022 ' + seg.segment + ' \u2014 ' + seg.rationale);\nlines.push('*Budget split:*');\nfor (const b of s.budget_split || []) lines.push(' \u2022 ' + b.segment + ': ' + b.allocation + ' (' + b.note + ')');\nif ((s.exclusions || []).length) lines.push('*Exclusions:* ' + s.exclusions.join(', '));\nlines.push('*Setup notes:*');\nfor (const n of s.setup_notes || []) lines.push(' \u2022 ' + n);\nlines.push('');\nlines.push('\u26a0\ufe0f *Primary risk:* ' + s.primary_risk);\nreturn [{ json: {\n plan_text: lines.join('\\n'),\n strategy_summary: s.strategy_summary || '',\n audience_approach: s.audience_approach || ''\n} }];"
},
"typeVersion": 2
},
{
"id": "acbe4c85-482e-4685-8d9e-575727073eef",
"name": "Slack: post strategy",
"type": "n8n-nodes-base.httpRequest",
"position": [
752,
848
],
"parameters": {
"url": "https://hooks.slack.com/services/REPLACE_WITH_YOUR_SLACK_WEBHOOK",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify({ text: $json.plan_text }) }}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.2
},
{
"id": "43b2e025-312c-4a35-a10f-3ab5649278a9",
"name": "OpenAI: draft ads from strategy",
"type": "n8n-nodes-base.httpRequest",
"position": [
1472,
448
],
"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 aimed at the exact audience the strategy names. Return ONLY JSON.' }, { role: 'user', content: 'Write 3 distinct ' + $('Campaign brief').first().json.platform + ' ad variants for this product: ' + $('Campaign brief').first().json.product + '. The targeting strategy: ' + $json.strategy_summary + ' Audience approach: ' + $json.audience_approach + '. 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": "6edea72c-5007-4904-8876-2e0da57f72db",
"name": "Split into variants",
"type": "n8n-nodes-base.code",
"position": [
1696,
448
],
"parameters": {
"jsCode": "const brief = $('Campaign brief').first().json;\nconst plan = $('Format the plan').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: plan.audience_approach || ''\n }\n}));"
},
"typeVersion": 2
},
{
"id": "d3d6257a-0617-4188-b244-c553d4634a28",
"name": "Spendict: assess_ad_creative",
"type": "n8n-nodes-base.httpRequest",
"position": [
2192,
448
],
"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": "3c1a8b62-9c5c-40fa-8727-a81f1018fd68",
"name": "Gate on verdict",
"type": "n8n-nodes-base.switch",
"position": [
2416,
448
],
"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": "a66b7dbe-9e25-45c2-9d9c-2eead83a7bc6",
"name": "Row: vetted variant",
"type": "n8n-nodes-base.code",
"position": [
2912,
448
],
"parameters": {
"jsCode": "const ad = $('Split into variants').item.json;\nconst fixes = $json.prioritized_fixes || [];\nreturn [{ json: {\n status: $json.launch_recommendation === 'run' ? 'shippable' : 'needs one fix',\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: fixes.length ? (fixes[0].suggestion || fixes[0].issue || '') : ''\n} }];"
},
"typeVersion": 2
},
{
"id": "e013037d-9cdb-46d0-b8ec-a23e9de803f4",
"name": "Google Sheets: log variants",
"type": "n8n-nodes-base.googleSheets",
"position": [
3136,
448
],
"parameters": {
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"typeVersion": 4.5
},
{
"id": "48897d00-d52f-4ad3-b0af-444c0141b5aa",
"name": "Launch pack",
"type": "n8n-nodes-base.aggregate",
"position": [
3376,
448
],
"parameters": {
"options": {},
"aggregate": "aggregateAllItemData"
},
"typeVersion": 1
},
{
"id": "693f6e18-04fa-4ac4-a594-584fef0f0a1d",
"name": "Slack: pack summary",
"type": "n8n-nodes-base.httpRequest",
"position": [
3616,
448
],
"parameters": {
"url": "https://hooks.slack.com/services/REPLACE_WITH_YOUR_SLACK_WEBHOOK",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify({ text: '\u2705 Launch pack ready: targeting strategy posted above + ' + ($json.data ? $json.data.length : 0) + ' vetted ad variant(s) logged to the sheet.' }) }}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.2
},
{
"id": "bab21b73-2068-45ed-90b7-4af9637245cc",
"name": "Killed drafts (logged)",
"type": "n8n-nodes-base.aggregate",
"position": [
2192,
848
],
"parameters": {
"options": {},
"aggregate": "aggregateAllItemData"
},
"typeVersion": 1
},
{
"id": "e7ff5245-31e2-48d8-abaf-05b4ac36d2f4",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
48,
128
],
"parameters": {
"width": 480,
"height": 992,
"content": "## Plan targeting and draft pre-vetted ads\n\nOne campaign brief in, a launch-ready pack out: Spendict designs the targeting strategy, OpenAI drafts ads against it, and every draft is scored before anything reaches your ad account.\n\n### How it works\n\n1. A form collects the campaign brief \u2014 product, budget, objective and platform.\n2. Spendict's `strategize_targeting` returns one opinionated plan: audience approach, segments, budget split and the primary risk.\n3. The formatted plan is posted to Slack, so the team sees the targeting call before any creative exists.\n4. OpenAI drafts ad variants aimed at that strategy's audience rather than generic copy.\n5. Spendict's `assess_ad_creative` scores every draft and returns a deterministic run / fix_first / kill verdict.\n6. Surviving variants are logged to Google Sheets and summarised to Slack as a launch pack. Killed drafts stop where they are.\n\n### Setup\n\n- **Spendict** \u2014 free key at spendict.com/dashboard, then Header Auth `Authorization: Bearer spd_live_\u2026` on both Spendict nodes.\n- **OpenAI** \u2014 Header Auth `Authorization: Bearer sk-\u2026` on the OpenAI node.\n- **Google Sheets** \u2014 connect Google and pick a sheet with headers `variant`, `headline`, `primary_text`, `verdict`, `score`, `top_fix`.\n- **Slack** \u2014 paste your Incoming Webhook URL into both Slack nodes.\n\n### Customization tips\n\nSwap the form trigger for a webhook or Airtable trigger to run this on a schedule."
},
"typeVersion": 1
},
{
"id": "00560203-a1d2-42a8-8d78-369dba954c96",
"name": "Section: brief to strategy",
"type": "n8n-nodes-base.stickyNote",
"position": [
656,
128
],
"parameters": {
"color": 7,
"width": 724,
"height": 892,
"content": "## Brief to strategy\n\nA short brief goes to Spendict, which returns one executable targeting plan \u2014 segments, budget split and the primary risk \u2014 then posts it to Slack."
},
"typeVersion": 1
},
{
"id": "8b3e0f73-9ab5-4e9a-bacb-493f903b542e",
"name": "Section: draft ads",
"type": "n8n-nodes-base.stickyNote",
"position": [
1392,
128
],
"parameters": {
"color": 7,
"width": 708,
"height": 540,
"content": "## Draft ads from strategy\n\nOpenAI writes variants aimed at the strategy's audience, then they are split one per item."
},
"typeVersion": 1
},
{
"id": "679ca648-0a8e-4ec9-bc45-35c9bbac2ff6",
"name": "Section: gate variants",
"type": "n8n-nodes-base.stickyNote",
"position": [
2112,
128
],
"parameters": {
"color": 7,
"width": 692,
"height": 892,
"content": "## Gate every variant\n\nSpendict scores each draft and returns run / fix_first / kill. Killed drafts stop here and are never logged as shippable."
},
"typeVersion": 1
},
{
"id": "3501aea3-4056-4c27-930f-3fd827b440d4",
"name": "Section: deliver pack",
"type": "n8n-nodes-base.stickyNote",
"position": [
2816,
128
],
"parameters": {
"color": 7,
"width": 1052,
"height": 524,
"content": "## Deliver the launch pack\n\nSurviving variants are logged to Google Sheets and summarised to Slack as one launch-ready pack."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "7f48414a-808a-4dda-b5d5-25580644f33f",
"nodeGroups": [],
"connections": {
"Launch pack": {
"main": [
[
{
"node": "Slack: pack summary",
"type": "main",
"index": 0
}
]
]
},
"Campaign brief": {
"main": [
[
{
"node": "Spendict: strategize_targeting",
"type": "main",
"index": 0
}
]
]
},
"Format the plan": {
"main": [
[
{
"node": "Slack: post strategy",
"type": "main",
"index": 0
},
{
"node": "OpenAI: draft ads from strategy",
"type": "main",
"index": 0
}
]
]
},
"Gate on verdict": {
"main": [
[
{
"node": "Row: vetted variant",
"type": "main",
"index": 0
}
],
[
{
"node": "Row: vetted variant",
"type": "main",
"index": 0
}
],
[
{
"node": "Killed drafts (logged)",
"type": "main",
"index": 0
}
]
]
},
"Row: vetted variant": {
"main": [
[
{
"node": "Google Sheets: log variants",
"type": "main",
"index": 0
}
]
]
},
"Split into variants": {
"main": [
[
{
"node": "Spendict: assess_ad_creative",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets: log variants": {
"main": [
[
{
"node": "Launch pack",
"type": "main",
"index": 0
}
]
]
},
"Spendict: assess_ad_creative": {
"main": [
[
{
"node": "Gate on verdict",
"type": "main",
"index": 0
}
]
]
},
"Spendict: strategize_targeting": {
"main": [
[
{
"node": "Format the plan",
"type": "main",
"index": 0
}
]
]
},
"OpenAI: draft ads from strategy": {
"main": [
[
{
"node": "Split into variants",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
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 before logging vetted variants to Google Sheets and posting updates to Slack.…
Source: https://n8n.io/workflows/17374/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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 Goog
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
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
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
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,