{
  "nodes": [
    {
      "id": "b07b40c1-4c8c-4c35-aa07-be7025ed68b2",
      "name": "Get Product Feed",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "CORRECTED: Uses v1 API if available, or replace with your e-commerce platform feed endpoint",
      "position": [
        -1380,
        820
      ],
      "parameters": {
        "url": "={{$env.CHANNABLE_API_URL}}/v1/projects/{{$env.PROJECT_ID}}/items",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "4b90a3bc-da44-44bc-b19a-d81427545f26",
      "name": "Format for CSV",
      "type": "n8n-nodes-base.set",
      "notes": "Formats compliant ads into CSV structure",
      "position": [
        560,
        700
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "bea6c2f0-4a95-4848-85a1-780cc048b26d",
      "name": "Alert - Non-Compliant",
      "type": "n8n-nodes-base.slack",
      "notes": "Sends Slack alert for non-compliant ads. Can replace with Email node.",
      "position": [
        560,
        900
      ],
      "parameters": {
        "text": "=\u26a0\ufe0f Non-Compliant Ad Flagged\n\n*Product ID:* {{$json.product_id}}\n*Product Title:* {{$json.product_title}}\n*Category:* {{$json.category}}\n\n*Generated Headline:* {{$json.validated_headline}}\n*Generated Description:* {{$json.validated_description}}\n\n*Compliance Issues:* Check agent output\n\n*Timestamp:* {{$json.validation_timestamp}}",
        "otherOptions": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "95f53395-e93d-4309-8f98-d2b2378cbb13",
      "name": "Aggregate Batches",
      "type": "n8n-nodes-base.aggregate",
      "notes": "Combines all processed batches into single dataset",
      "position": [
        760,
        700
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "56492a38-1188-4b35-999f-14017ac76484",
      "name": "Save to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "CORRECTED: Saves CSV data to Google Sheets instead of Channable API upload (which doesn't exist). You can then: 1) Import manually to Google Ads, 2) Use Channable scheduled import, or 3) Build Google Ads API direct upload",
      "position": [
        1160,
        700
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Generated Ads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{$env.GOOGLE_SHEET_ID}}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "f25d680c-28ad-43f5-96c8-0f022cf1a710",
      "name": "Schedule Trigger - Daily1",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "Triggers workflow daily at midnight. Can also be triggered manually.",
      "position": [
        -1600,
        820
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 0 * * *"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "01a193ae-a04f-4a04-9c3e-0a40c8e197c5",
      "name": "Split Into Batches1",
      "type": "n8n-nodes-base.splitInBatches",
      "notes": "Processes 50 products at a time to avoid API rate limits",
      "position": [
        -1140,
        820
      ],
      "parameters": {
        "options": {},
        "batchSize": 50
      },
      "typeVersion": 3
    },
    {
      "id": "50792985-2a29-4aeb-8191-2a1ef3d7ec97",
      "name": "Generate Ad Copy - Relevance AI1",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "CORRECTED: Uses /trigger endpoint with tool ID from environment variable. Get your actual tool ID after creating in Relevance AI.",
      "position": [
        -880,
        800
      ],
      "parameters": {
        "url": "={{$env.RELEVANCE_AI_API_URL}}/tools/google_text_ad_copy_generator/run",
        "method": "POST",
        "options": {
          "timeout": 60000
        },
        "jsonBody": "={\n  \"params\": {\n    \"product_title\": \"{{$json.title}}\",\n    \"product_description\": \"{{$json.description}}\",\n    \"price\": \"{{$json.price}}\",\n    \"category\": \"{{$json.category}}\",\n    \"brand\": \"{{$json.brand}}\"\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "17d67f83-8300-41c5-a3ac-9b7ea4bc25c2",
      "name": "Validate Character Limits1",
      "type": "n8n-nodes-base.code",
      "notes": "Validates headline \u226430 chars and description \u226490 chars. This is the fix from the YouTube video for accurate character counting.",
      "position": [
        -560,
        800
      ],
      "parameters": {
        "jsCode": "// Character limit validation with accurate counting\n\nconst headline = $input.item.json.headline || '';\nconst description = $input.item.json.description || '';\n\n// Function to accurately count displayed characters\nfunction getCharacterCount(str) {\n  str = str.trim();\n  return str.length;\n}\n\n// Validate headline (max 30 characters)\nlet headlineResult = {};\nconst headlineCount = getCharacterCount(headline);\n\nif (headlineCount > 30) {\n  let truncated = headline.substring(0, 27).trim();\n  truncated = truncated.replace(/[,.\\-:]$/, '');\n  headlineResult = {\n    headline: truncated + '...',\n    char_count: truncated.length + 3,\n    truncated: true,\n    original: headline\n  };\n} else {\n  headlineResult = {\n    headline: headline,\n    char_count: headlineCount,\n    truncated: false,\n    original: headline\n  };\n}\n\n// Validate description (max 90 characters)\nlet descriptionResult = {};\nconst descriptionCount = getCharacterCount(description);\n\nif (descriptionCount > 90) {\n  let truncated = description.substring(0, 87).trim();\n  truncated = truncated.replace(/[,.\\-:]$/, '');\n  descriptionResult = {\n    description: truncated + '...',\n    char_count: truncated.length + 3,\n    truncated: true,\n    original: description\n  };\n} else {\n  descriptionResult = {\n    description: description,\n    char_count: descriptionCount,\n    truncated: false,\n    original: description\n  };\n}\n\n// Return validated data with original product info\nreturn {\n  product_id: $input.item.json.product_id || $input.item.json.id,\n  product_title: $input.item.json.product_title || $input.item.json.title,\n  validated_headline: headlineResult.headline,\n  headline_char_count: headlineResult.char_count,\n  headline_truncated: headlineResult.truncated,\n  validated_description: descriptionResult.description,\n  description_char_count: descriptionResult.char_count,\n  description_truncated: descriptionResult.truncated,\n  category: $input.item.json.category,\n  final_url: $input.item.json.product_url || $input.item.json.link,\n  validation_timestamp: new Date().toISOString()\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "534ea658-ea94-4d4c-a4e3-5d5fa1c699b6",
      "name": "Compliance Check Agent1",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "CORRECTED: Uses /agents/trigger with agent_id in body. Get your actual agent ID from Relevance AI.",
      "position": [
        -160,
        800
      ],
      "parameters": {
        "url": "={{$env.RELEVANCE_AI_API_URL}}/agents/google_ads_compliance_checker/run",
        "method": "POST",
        "options": {
          "timeout": 60000
        },
        "jsonBody": "={\n  \"message\": {\n    \"role\": \"user\",\n    \"content\": \"Check compliance for this ad: Headline: {{$json.validated_headline}}, Description: {{$json.validated_description}}, Category: {{$json.category}}\"\n  },\n  \"agent_id\": \"{{$env.RELEVANCE_AGENT_COMPLIANCE_ID}}\"\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "c9bfe847-7215-4ae5-a61e-4eb24ae1da0c",
      "name": "IF Compliant1",
      "type": "n8n-nodes-base.if",
      "notes": "Routes ads based on compliance status. APPROVED ads go to formatting, others trigger alert.",
      "position": [
        160,
        800
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "string": [
            {
              "value1": "={{$json.compliance_status || $json.output}}",
              "value2": "APPROVED",
              "operation": "contains"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "5a27e0f1-d4e7-4a81-9700-dd1c368fbb0b",
      "name": "Generate CSV File1",
      "type": "n8n-nodes-base.code",
      "notes": "Converts JSON to properly escaped CSV format",
      "position": [
        960,
        700
      ],
      "parameters": {
        "jsCode": "// Generate CSV from aggregated data\n\nconst items = $input.all();\n\n// CSV headers\nconst headers = ['product_id', 'headline', 'description', 'final_url', 'display_url'];\n\n// Create CSV rows\nconst rows = items.map(item => {\n  const json = item.json;\n  return [\n    json.product_id || '',\n    json.headline || '',\n    json.description || '',\n    json.final_url || '',\n    json.display_url || ''\n  ];\n});\n\n// Convert to CSV string\nconst csvRows = [headers, ...rows];\nconst csvString = csvRows\n  .map(row => row.map(cell => `\"${String(cell).replace(/\"/g, '\"\"')}\"`).join(','))\n  .join('\\n');\n\n// Return CSV data\nreturn {\n  csv_data: csvString,\n  total_ads: rows.length,\n  generated_at: new Date().toISOString(),\n  filename: `google_ads_${new Date().toISOString().split('T')[0]}.csv`\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "55d4429b-b4a9-4a19-bb34-2f58d7881a5c",
      "name": "Success Notification1",
      "type": "n8n-nodes-base.slack",
      "notes": "Sends success notification with summary",
      "position": [
        1360,
        700
      ],
      "parameters": {
        "text": "=\u2705 *Google Ads Generation Complete*\n\n\ud83d\udcca *Summary:*\n\u2022 Total Ads Generated: {{$node['Generate CSV File1'].json.total_ads}}\n\u2022 Saved to Google Sheets: {{$env.GOOGLE_SHEET_ID}}\n\u2022 Timestamp: {{$node['Generate CSV File1'].json.generated_at}}\n\n\ud83c\udfaf *Next Steps:*\n\u2022 Review ads in Google Sheet\n\u2022 Import to Google Ads (manual or scheduled)\n\u2022 Monitor for disapprovals in 24 hours\n\n\ud83d\udca1 CSV filename: {{$node['Generate CSV File1'].json.filename}}",
        "otherOptions": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "9af47779-8ca3-4e7c-9d3d-76f419c45361",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2060,
        760
      ],
      "parameters": {
        "width": 360,
        "height": 220,
        "content": "## \ud83d\udfe6 Schedule Trigger - Daily\n\n### \ud83d\udd53 Purpose: Automatically runs every night at midnight (0 0 * * *).\nTip: You can also execute manually for testing.\n\n\ud83d\udca1 Use to refresh and generate new ad copy daily."
      },
      "typeVersion": 1
    },
    {
      "id": "d563b4b9-7239-4bb1-ab62-73b924643715",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1500,
        400
      ],
      "parameters": {
        "width": 340,
        "height": 320,
        "content": "## \ud83d\udfe6 Get Product Feed (Channable)\n### \ud83d\udce6 Purpose: Fetches live product data (title, price, brand, description, category).\nAPI: GET {{$env.CHANNABLE_API_URL}}/v1/projects/{{$env.PROJECT_ID}}/items\n\n#### \u2699\ufe0f Replace or connect to your store\u2019s API if Channable isn\u2019t used.\n\u2705 Returns all active products for ad generation."
      },
      "typeVersion": 1
    },
    {
      "id": "538ff6d6-5496-42db-90c4-2f22a1f228f4",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1240,
        980
      ],
      "parameters": {
        "width": 320,
        "height": 240,
        "content": "## \ud83d\udfe6 Split Into Batches\n\n### \ud83e\uddee Purpose: Processes up to 50 products per batch.\n\nPrevents hitting API rate limits from Relevance AI or Channable.\n\u26a1 Adjust batch size depending on product volume and plan limits."
      },
      "typeVersion": 1
    },
    {
      "id": "bb106886-79a8-437c-8f74-50939befe720",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1020,
        400
      ],
      "parameters": {
        "width": 340,
        "height": 320,
        "content": "## \ud83d\udfe6 Generate Ad Copy - Relevance AI\n### \u270d\ufe0f Purpose: Calls your Relevance AI Tool to generate Google Ads headlines & descriptions.\nAPI: POST /tools/google_text_ad_copy_generator/run\n\nUses product title, description, price, brand, and category.\n\ud83e\udde0 Model: Claude 3.5 / GPT-4 via Relevance AI."
      },
      "typeVersion": 1
    },
    {
      "id": "eabe3ce0-d216-426d-8cc3-e1411f36c385",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -680,
        980
      ],
      "parameters": {
        "width": 320,
        "height": 300,
        "content": "## \ud83d\udfe6 Validate Character Limits\n\n\ud83d\udd0d Purpose: Ensures headlines \u226430 chars and descriptions \u226490 chars.\n\nUses JavaScript to count accurately and truncate gracefully.\n\u2728 Automatically cleans trailing punctuation and spaces.\n\ud83d\udca1 Fixes the common issue where ads get disapproved for \u201ctoo long\u201d text."
      },
      "typeVersion": 1
    },
    {
      "id": "adb78971-8b7e-4eaf-9d9a-c47439b35c12",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        400
      ],
      "parameters": {
        "width": 340,
        "height": 320,
        "content": "## \ud83d\udfe6 Compliance Check Agent\n\n\ud83e\udde0 Purpose: Checks generated ad text for Google Ads policy compliance.\nAPI: POST /agents/google_ads_compliance_checker/run\n\nFlags emojis, exaggerated claims, restricted terms, etc.\nReturns \"APPROVED\" or \"REJECTED\".\n\ud83d\udd10 Uses your Relevance AI Agent ID."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "IF Compliant1": {
      "main": [
        [
          {
            "node": "Format for CSV",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Alert - Non-Compliant",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format for CSV": {
      "main": [
        [
          {
            "node": "Aggregate Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Product Feed": {
      "main": [
        [
          {
            "node": "Split Into Batches1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Batches": {
      "main": [
        [
          {
            "node": "Generate CSV File1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate CSV File1": {
      "main": [
        [
          {
            "node": "Save to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Into Batches1": {
      "main": [
        [
          {
            "node": "Generate Ad Copy - Relevance AI1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Google Sheets": {
      "main": [
        [
          {
            "node": "Success Notification1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compliance Check Agent1": {
      "main": [
        [
          {
            "node": "IF Compliant1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger - Daily1": {
      "main": [
        [
          {
            "node": "Get Product Feed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Character Limits1": {
      "main": [
        [
          {
            "node": "Compliance Check Agent1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Ad Copy - Relevance AI1": {
      "main": [
        [
          {
            "node": "Validate Character Limits1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}