{
  "name": "AI Ad Generation Pipeline",
  "nodes": [
    {
      "id": "dd5f24cc-2488-4d20-8cbd-b670cc3af5c8",
      "name": "Every Minute",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        752
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 1
            }
          ]
        }
      }
    },
    {
      "id": "aa5e3b67-5586-48e6-bcfd-1eb3f1538deb",
      "name": "Find New Briefs",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        224,
        752
      ],
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "search",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_AIRTABLE_BASE_ID",
          "cachedResultName": "GlowKit Ad Briefs"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_AIRTABLE_TABLE_ID",
          "cachedResultName": "Ad Briefs"
        },
        "filterByFormula": "{Status}=\"New\"",
        "returnAll": true,
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "1f4a3dff-e6e9-402a-8340-b72c9d704de5",
      "name": "Mark Processing",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        448,
        752
      ],
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "update",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_AIRTABLE_BASE_ID",
          "cachedResultName": "GlowKit Ad Briefs"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_AIRTABLE_TABLE_ID",
          "cachedResultName": "Ad Briefs"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "value": {
            "id": "={{ $json.id }}",
            "Status": "Processing"
          }
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "b607951a-7fe5-414c-a747-9066792de812",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        672,
        752
      ],
      "parameters": {
        "batchSize": 1
      }
    },
    {
      "id": "bb20ebb7-46d1-4b37-bd8b-d628d8436a19",
      "name": "Generate Ad Copy",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        896,
        240
      ],
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.fields.Brief }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are the in-house copywriter for GlowKit, a direct-to-consumer skincare brand that sells on Meta Ads (Instagram/Facebook).\n\nBrand voice: Friendly, confident, science-backed but not clinical. Short, punchy sentences. Never use hype words like \"miracle\", \"magic\", or \"revolutionary\".\n\nAudience: Women 25-40, skincare-curious, shop on Instagram.\n\nProduct context: Vitamin C serum, hydrating cleanser, and bundle deals.\n\nGiven a one-line ad brief, produce:\n- Exactly 3 short scroll-stopping hooks (each a single strong opening line/headline for the ad's hook).\n- One Meta primary text (roughly 125 words or fewer) in the GlowKit voice that expands on the best hook.\n- One headline of 40 characters or fewer.\n- One clear call-to-action (e.g. \"Shop Now\", \"Get 20% Off\").\n- One detailed image_prompt for an AI image generator: product-focused, natural light, Instagram-feed aesthetic, no text or typography rendered in the image itself.\n\nBase everything on the brief you are given. Do not invent claims that aren't implied by the brief."
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "8f17e5d9-f9b1-4ebb-91fe-39df0cb88344",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        912,
        464
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini",
          "cachedResultName": "gpt-5-mini"
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "2cb4a8c8-c4d1-4fb0-b5a1-1266a1a1e1fd",
      "name": "Ad Copy Schema",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        1040,
        464
      ],
      "parameters": {
        "schemaType": "fromJson",
        "jsonSchemaExample": "{\"hooks\":[\"Hook one text\",\"Hook two text\",\"Hook three text\"],\"primary_text\":\"Primary ad text in GlowKit's voice, about 125 words or fewer.\",\"headline\":\"Short headline, 40 chars or fewer\",\"cta\":\"Shop Now\",\"image_prompt\":\"Detailed, product-focused, Instagram-style image prompt with no text in the image.\"}"
      }
    },
    {
      "id": "c3ceb2c5-e567-4782-ab4a-018bf875c5b3",
      "name": "Parse & Validate Ad Copy",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1248,
        176
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const raw = $input.item.json.output ?? $input.item.json;\nconst hooks = raw.hooks;\nconst primaryText = raw.primary_text;\nconst headline = raw.headline;\nconst cta = raw.cta;\nconst imagePrompt = raw.image_prompt;\n\nif (!Array.isArray(hooks) || hooks.length !== 3 || hooks.some((h) => typeof h !== 'string' || !h.trim())) {\n  throw new Error('Invalid ad copy: expected exactly 3 non-empty hooks, got ' + JSON.stringify(hooks));\n}\nif (typeof headline !== 'string' || !headline.trim() || headline.length > 40) {\n  throw new Error('Invalid ad copy: headline must be a non-empty string of 40 characters or fewer, got \"' + headline + '\"');\n}\nif (typeof primaryText !== 'string' || !primaryText.trim()) {\n  throw new Error('Invalid ad copy: primary_text is missing or empty');\n}\nconst wordCount = primaryText.trim().split(/\\s+/).length;\nif (wordCount > 150) {\n  throw new Error('Invalid ad copy: primary_text is too long (' + wordCount + ' words, max ~125)');\n}\nif (typeof cta !== 'string' || !cta.trim()) {\n  throw new Error('Invalid ad copy: cta is missing or empty');\n}\nif (typeof imagePrompt !== 'string' || !imagePrompt.trim()) {\n  throw new Error('Invalid ad copy: image_prompt is missing or empty');\n}\n\nreturn {\n  json: {\n    hook1: hooks[0],\n    hook2: hooks[1],\n    hook3: hooks[2],\n    primaryText: primaryText,\n    headline: headline,\n    cta: cta,\n    imagePrompt: imagePrompt\n  }\n};"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "48e930ac-454f-4e29-ae0c-42e8f8bae0cf",
      "name": "Generate Ad Image",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.3,
      "position": [
        1472,
        144
      ],
      "parameters": {
        "resource": "image",
        "operation": "generate",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-image-1-mini",
          "cachedResultName": "gpt-image-1-mini"
        },
        "prompt": "={{ $json.imagePrompt }}",
        "options": {
          "size": "1024x1024",
          "quality": "medium",
          "binaryPropertyOutput": "data"
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "1444c1aa-40e4-4c0b-a4b2-451ac1336907",
      "name": "Prepare Image Upload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1696,
        144
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const bin = $input.item.binary && $input.item.binary.data;\nif (!bin || !bin.data) {\n  throw new Error('OpenAI image generation did not return binary image data');\n}\nreturn {\n  json: {\n    imageBase64: bin.data,\n    mimeType: bin.mimeType || 'image/png',\n    fileName: bin.fileName || 'glowkit-ad-image.png'\n  }\n};"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "eb643300-1a2d-489b-9545-2dfe364dd147",
      "name": "Upload Image to Airtable",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1920,
        80
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://content.airtable.com/v0/YOUR_AIRTABLE_BASE_ID/{{ $('Loop Over Items').item.json.id }}/fld2oFdcjRI4HsG7o/uploadAttachment",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "airtableTokenApi",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ { \"contentType\": $json.mimeType, \"file\": $json.imageBase64, \"filename\": $json.fileName } }}",
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "b47b3dce-0cfd-4368-af22-0875df1f1a68",
      "name": "Write Ad Creative",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        2144,
        0
      ],
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "update",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_AIRTABLE_BASE_ID",
          "cachedResultName": "GlowKit Ad Briefs"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_AIRTABLE_TABLE_ID",
          "cachedResultName": "Ad Briefs"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "value": {
            "id": "={{ $('Loop Over Items').item.json.id }}",
            "Hook 1": "={{ $('Parse & Validate Ad Copy').item.json.hook1 }}",
            "Hook 2": "={{ $('Parse & Validate Ad Copy').item.json.hook2 }}",
            "Hook 3": "={{ $('Parse & Validate Ad Copy').item.json.hook3 }}",
            "Primary Text": "={{ $('Parse & Validate Ad Copy').item.json.primaryText }}",
            "Headline": "={{ $('Parse & Validate Ad Copy').item.json.headline }}",
            "CTA": "={{ $('Parse & Validate Ad Copy').item.json.cta }}",
            "Status": "Ready for Review"
          }
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "d41af511-9083-4c2b-a31c-bf84c3166ed8",
      "name": "Notify Ready",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        2368,
        144
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "you@yourcompany.com",
        "subject": "=\u2705 New ad variants ready for GlowKit \u2014 {{ $('Loop Over Items').item.json.fields.Brief.slice(0, 60) }}",
        "emailType": "text",
        "message": "=New GlowKit ad creative is ready for review.\n\nBrief: {{ $(\"Loop Over Items\").item.json.fields.Brief }}\n\nHeadline: {{ $(\"Parse & Validate Ad Copy\").item.json.headline }}\nCTA: {{ $(\"Parse & Validate Ad Copy\").item.json.cta }}\n\nOpen the GlowKit Ad Briefs base in Airtable to review and approve.",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "ff536191-8a48-422d-81fd-c4d102330613",
      "name": "Mark Error",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        2368,
        384
      ],
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "update",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_AIRTABLE_BASE_ID",
          "cachedResultName": "GlowKit Ad Briefs"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_AIRTABLE_TABLE_ID",
          "cachedResultName": "Ad Briefs"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "value": {
            "id": "={{ $('Loop Over Items').item.json.id }}",
            "Status": "Error",
            "Notes": "={{ $json.error?.message ?? $json.message ?? \"Unknown error during ad generation\" }}"
          }
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "4a05561b-319e-4227-bf0f-218e25478eb9",
      "name": "Notify Failure",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        2592,
        672
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "you@yourcompany.com",
        "subject": "=\u26a0\ufe0f Ad generation failed for GlowKit \u2014 {{ $('Loop Over Items').item.json.fields.Brief.slice(0, 60) }}",
        "emailType": "text",
        "message": "=Ad generation failed for a GlowKit brief and the record was marked Error.\n\nBrief: {{ $(\"Loop Over Items\").item.json.fields.Brief }}\nError: {{ $(\"Mark Error\").item.json.fields.Notes }}\n\nOpen the GlowKit Ad Briefs base in Airtable to review the Notes field.",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    }
  ],
  "connections": {
    "Every Minute": {
      "main": [
        [
          {
            "node": "Find New Briefs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find New Briefs": {
      "main": [
        [
          {
            "node": "Mark Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Processing": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Generate Ad Copy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Ad Copy": {
      "main": [
        [
          {
            "node": "Parse & Validate Ad Copy",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Ad Copy",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Ad Copy Schema": {
      "ai_outputParser": [
        [
          {
            "node": "Generate Ad Copy",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Validate Ad Copy": {
      "main": [
        [
          {
            "node": "Generate Ad Image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Ad Image": {
      "main": [
        [
          {
            "node": "Prepare Image Upload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Image Upload": {
      "main": [
        [
          {
            "node": "Upload Image to Airtable",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Image to Airtable": {
      "main": [
        [
          {
            "node": "Write Ad Creative",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Ad Creative": {
      "main": [
        [
          {
            "node": "Notify Ready",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify Ready": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Error": {
      "main": [
        [
          {
            "node": "Notify Failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify Failure": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}