{
  "name": "Analyze competitor YouTube thumbnails with OpenAI and save approved briefs to Google Sheets",
  "nodes": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "name": "Collect Video Brief and Competitors",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -1400,
        80
      ],
      "parameters": {
        "options": {
          "path": "thumbcanvas-competitor-analyst",
          "ignoreBots": true,
          "buttonLabel": "Analyze competitor thumbnails",
          "showHeaders": false,
          "appendAttribution": true,
          "useWorkflowTimezone": true
        },
        "formTitle": "Competitor-informed YouTube thumbnail brief",
        "formFields": {
          "values": [
            {
              "fieldName": "planned_video_title",
              "fieldType": "text",
              "fieldLabel": "Planned video title",
              "placeholder": "e.g. I survived 100 days in a frozen world",
              "requiredField": true
            },
            {
              "fieldName": "video_promise",
              "fieldType": "textarea",
              "fieldLabel": "Video promise",
              "placeholder": "What happens, changes, or gets resolved?",
              "requiredField": true
            },
            {
              "fieldName": "audience",
              "fieldType": "text",
              "fieldLabel": "Audience",
              "placeholder": "e.g. Minecraft survival viewers",
              "requiredField": true
            },
            {
              "fieldName": "target_keyword",
              "fieldType": "text",
              "fieldLabel": "Target keyword",
              "placeholder": "e.g. 100 days Minecraft",
              "requiredField": true
            },
            {
              "fieldName": "brand_palette",
              "fieldType": "text",
              "fieldLabel": "Brand palette",
              "placeholder": "e.g. ice blue, orange accent, charcoal"
            },
            {
              "fieldName": "thumbnail_text",
              "fieldType": "text",
              "fieldLabel": "Optional thumbnail text (four words maximum)",
              "placeholder": "e.g. 100 DAYS"
            },
            {
              "fieldName": "competitor_url_1",
              "fieldType": "text",
              "fieldLabel": "Competitor YouTube URL 1",
              "placeholder": "https://www.youtube.com/watch?v=...",
              "requiredField": true
            },
            {
              "fieldName": "competitor_url_2",
              "fieldType": "text",
              "fieldLabel": "Competitor YouTube URL 2",
              "placeholder": "https://youtu.be/...",
              "requiredField": true
            },
            {
              "fieldName": "competitor_url_3",
              "fieldType": "text",
              "fieldLabel": "Competitor YouTube URL 3 (optional)",
              "placeholder": "https://www.youtube.com/shorts/..."
            }
          ]
        },
        "responseMode": "lastNode",
        "authentication": "none",
        "formDescription": "Analyze two or three public competitor thumbnails, generate three original directions, and approve one before a ThumbCanvas link is created. Do not enter confidential information."
      },
      "typeVersion": 2.6
    },
    {
      "id": "22222222-2222-4222-8222-222222222222",
      "name": "Normalize and Validate Brief",
      "type": "n8n-nodes-base.code",
      "notes": "Accepts only public HTTPS YouTube watch, youtu.be, Shorts, or embed URLs. It deduplicates video IDs and rejects thumbnail text longer than four words.",
      "position": [
        -1160,
        80
      ],
      "parameters": {
        "jsCode": "const clean = (value, max) => String(value ?? '').replace(/\\s+/g, ' ').trim().slice(0, max);\n\nconst plannedVideoTitle = clean($json.planned_video_title, 140);\nconst videoPromise = clean($json.video_promise, 600);\nconst audience = clean($json.audience, 120);\nconst targetKeyword = clean($json.target_keyword, 100);\nconst brandPalette = clean($json.brand_palette, 120) || 'high contrast, channel-consistent colors';\nconst thumbnailText = clean($json.thumbnail_text, 40);\nconst rawUrls = [\n  clean($json.competitor_url_1, 300),\n  clean($json.competitor_url_2, 300),\n  clean($json.competitor_url_3, 300),\n].filter(Boolean);\n\nconst extractYouTubeId = (value) => {\n  const patterns = [\n    /^https:\\/\\/(?:www\\.|m\\.)?youtube\\.com\\/watch\\?(?:[^#]*&)?v=([A-Za-z0-9_-]{11})(?:[&#]|$)/i,\n    /^https:\\/\\/youtu\\.be\\/([A-Za-z0-9_-]{11})(?:[?&#/]|$)/i,\n    /^https:\\/\\/(?:www\\.|m\\.)?youtube\\.com\\/(?:shorts|embed)\\/([A-Za-z0-9_-]{11})(?:[?&#/]|$)/i,\n  ];\n  for (const pattern of patterns) {\n    const match = value.match(pattern);\n    if (match) return match[1];\n  }\n  return '';\n};\n\nconst errors = [];\nif (!plannedVideoTitle) errors.push('Planned video title is required.');\nif (!videoPromise) errors.push('Video promise is required.');\nif (!audience) errors.push('Audience is required.');\nif (!targetKeyword) errors.push('Target keyword is required.');\nif (thumbnailText && thumbnailText.split(/\\s+/).filter(Boolean).length > 4) {\n  errors.push('Thumbnail text must contain four words or fewer.');\n}\nif (rawUrls.length < 2) errors.push('Add at least two public competitor YouTube URLs.');\n\nconst competitors = [];\nconst seen = new Set();\nfor (const sourceUrl of rawUrls) {\n  const videoId = extractYouTubeId(sourceUrl);\n  if (!videoId) {\n    errors.push('Unsupported YouTube URL: ' + sourceUrl);\n    continue;\n  }\n  if (seen.has(videoId)) continue;\n  seen.add(videoId);\n  competitors.push({\n    video_id: videoId,\n    source_url: 'https://www.youtube.com/watch?v=' + videoId,\n    thumbnail_url: 'https://i.ytimg.com/vi/' + videoId + '/hqdefault.jpg',\n    oembed_url: 'https://www.youtube.com/oembed?url=' + encodeURIComponent('https://www.youtube.com/watch?v=' + videoId) + '&format=json',\n  });\n}\nif (rawUrls.length >= 2 && competitors.length < 2) {\n  errors.push('Add at least two unique, supported YouTube video URLs.');\n}\n\nreturn [{\n  json: {\n    valid: errors.length === 0,\n    errors,\n    planned_video_title: plannedVideoTitle,\n    video_promise: videoPromise,\n    audience,\n    target_keyword: targetKeyword,\n    brand_palette: brandPalette,\n    thumbnail_text: thumbnailText,\n    aspect_ratio: '16:9',\n    competitors,\n    submitted_at: $json.submittedAt || new Date().toISOString(),\n    workflow_version: '2.0.0',\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "33333333-3333-4333-8333-333333333333",
      "name": "Brief Is Valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        -920,
        80
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "30303030-3030-4030-8030-303030303030",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.valid }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "44444444-4444-4444-8444-444444444444",
      "name": "Set Template Configuration",
      "type": "n8n-nodes-base.set",
      "notes": "Change the OpenAI model or ThumbCanvas destination here. Select Google Sheets credentials and destination in the append node near the end.",
      "position": [
        -680,
        -20
      ],
      "parameters": {
        "mode": "manual",
        "options": {
          "dotNotation": true
        },
        "assignments": {
          "assignments": [
            {
              "id": "31313131-3131-4131-8131-313131313131",
              "name": "openai_model",
              "type": "string",
              "value": "gpt-4.1-mini"
            },
            {
              "id": "32323232-3232-4232-8232-323232323232",
              "name": "thumbcanvas_base_url",
              "type": "string",
              "value": "https://thumbcanvas.com/create-thumbs"
            },
            {
              "id": "34343434-3434-4434-8434-343434343434",
              "name": "analysis_policy",
              "type": "string",
              "value": "Creative evidence only; never predict or guarantee CTR."
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.5
    },
    {
      "id": "55555555-5555-4555-8555-555555555555",
      "name": "Prepare Competitor Jobs",
      "type": "n8n-nodes-base.code",
      "notes": "Creates one n8n item per unique competitor. HTTP Request and OpenAI then process the items independently without a manual loop.",
      "position": [
        -440,
        -20
      ],
      "parameters": {
        "jsCode": "const brief = $json;\nreturn brief.competitors.map((competitor, index) => ({\n  json: {\n    ...competitor,\n    competitor_number: index + 1,\n    planned_video_title: brief.planned_video_title,\n    video_promise: brief.video_promise,\n    audience: brief.audience,\n    target_keyword: brief.target_keyword,\n    brand_palette: brief.brand_palette,\n    thumbnail_text: brief.thumbnail_text,\n    aspect_ratio: brief.aspect_ratio,\n    submitted_at: brief.submitted_at,\n    workflow_version: brief.workflow_version,\n    openai_model: brief.openai_model,\n    thumbcanvas_base_url: brief.thumbcanvas_base_url,\n  },\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "66666666-6666-4666-8666-666666666666",
      "name": "Fetch Public YouTube Metadata",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Uses YouTube's public oEmbed endpoint. No scraping service or YouTube API key is required.",
      "onError": "continueRegularOutput",
      "position": [
        -200,
        -20
      ],
      "parameters": {
        "url": "={{ $json.oembed_url }}",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true,
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "77777777-7777-4777-8777-777777777777",
      "name": "Combine Public YouTube Metadata",
      "type": "n8n-nodes-base.code",
      "position": [
        40,
        -20
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const job = $('Prepare Competitor Jobs').item.json;\nconst statusCode = Number($json.statusCode ?? 0);\nconst body = $json.body && typeof $json.body === 'object' ? $json.body : {};\nconst metadataOk = statusCode >= 200 && statusCode < 300 && Boolean(body.title);\n\nreturn {\n  json: {\n    ...job,\n    metadata_ok: metadataOk,\n    metadata_status: statusCode || 'request_error',\n    source_title: String(body.title || 'Metadata unavailable').slice(0, 240),\n    author_name: String(body.author_name || 'Unknown channel').slice(0, 160),\n    author_url: String(body.author_url || '').slice(0, 300),\n    thumbnail_url: String(body.thumbnail_url || job.thumbnail_url).slice(0, 500),\n  },\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-4888-8888-888888888888",
      "name": "Analyze Each Thumbnail with OpenAI Vision",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "notes": "Sends the public thumbnail URL and limited public metadata to OpenAI. High image detail improves inspection but does not eliminate vision errors.",
      "onError": "continueRegularOutput",
      "position": [
        280,
        -20
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.openai_model }}"
        },
        "options": {
          "store": false,
          "maxTokens": 900,
          "textFormat": {
            "textOptions": {
              "name": "thumbnail_visual_analysis",
              "type": "json_schema",
              "schema": "{\"type\":\"object\",\"properties\":{\"focal_subject\":{\"type\":\"string\"},\"visible_text\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"composition\":{\"type\":\"string\"},\"contrast\":{\"type\":\"string\"},\"emotion\":{\"type\":\"string\"},\"visual_hook\":{\"type\":\"string\"},\"copy_risks\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"transferable_patterns\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"confidence\":{\"type\":\"number\"},\"limitations\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"additionalProperties\":false,\"required\":[\"focal_subject\",\"visible_text\",\"composition\",\"contrast\",\"emotion\",\"visual_hook\",\"copy_risks\",\"transferable_patterns\",\"confidence\",\"limitations\"]}",
              "strict": true,
              "verbosity": "low",
              "description": "Visible evidence from one public YouTube thumbnail"
            }
          },
          "temperature": 0.2
        },
        "resource": "text",
        "simplify": true,
        "operation": "response",
        "responses": {
          "values": [
            {
              "role": "system",
              "type": "text",
              "content": "You are a conservative visual analyst. Describe only visible thumbnail evidence. Do not infer views, click-through rate, audience response, or creator intent. Return the required JSON schema."
            },
            {
              "role": "user",
              "type": "text",
              "content": "={{ 'Analyze this public YouTube thumbnail. Video ID: ' + $json.video_id + '. Public title: ' + $json.source_title + '. Channel: ' + $json.author_name + '. Identify the focal subject, visible text, composition, contrast, emotion, visual hook, transferable design patterns, copy risks, confidence from 0 to 1, and limitations. Small or uncertain text must be marked uncertain.' }}"
            },
            {
              "role": "user",
              "type": "image",
              "imageUrl": "={{ $json.thumbnail_url }}",
              "imageType": "url",
              "imageDetail": "high"
            }
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "99999999-9999-4999-8999-999999999999",
      "name": "Validate Structured Visual Analysis",
      "type": "n8n-nodes-base.code",
      "position": [
        520,
        -20
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const source = $('Combine Public YouTube Metadata').item.json;\nconst output = Array.isArray($json.output) ? $json.output : [];\nlet structured = null;\n\nfor (const item of output) {\n  if (!item || item.type !== 'message' || !Array.isArray(item.content)) continue;\n  const textPart = item.content.find((part) => part && part.type === 'output_text');\n  if (!textPart) continue;\n  if (textPart.text && typeof textPart.text === 'object') {\n    structured = textPart.text;\n  } else if (typeof textPart.text === 'string') {\n    try {\n      structured = JSON.parse(textPart.text);\n    } catch (error) {\n      structured = null;\n    }\n  }\n}\n\nconst requiredStrings = ['focal_subject', 'composition', 'contrast', 'emotion', 'visual_hook'];\nconst validShape = structured &&\n  requiredStrings.every((key) => typeof structured[key] === 'string') &&\n  Array.isArray(structured.visible_text) &&\n  Array.isArray(structured.copy_risks) &&\n  Array.isArray(structured.transferable_patterns) &&\n  Array.isArray(structured.limitations);\n\nreturn {\n  json: {\n    video_id: source.video_id,\n    source_url: source.source_url,\n    thumbnail_url: source.thumbnail_url,\n    source_title: source.source_title,\n    author_name: source.author_name,\n    metadata_ok: source.metadata_ok,\n    analysis_ok: Boolean(validShape),\n    analysis: validShape ? structured : null,\n    analysis_error: validShape ? '' : String($json.error?.message || $json.error || 'OpenAI did not return the required analysis schema').slice(0, 500),\n  },\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
      "name": "Aggregate Competitor Evidence",
      "type": "n8n-nodes-base.code",
      "notes": "Combines successful analyses, keeps failures visible, and labels the evidence state as complete, partial, or failed.",
      "position": [
        760,
        -20
      ],
      "parameters": {
        "jsCode": "const rows = $input.all().map((item) => item.json);\nconst brief = $('Set Template Configuration').first().json;\nconst usable = rows.filter((row) => row.analysis_ok);\nconst analysisStatus = usable.length === rows.length ? 'complete' : usable.length > 0 ? 'partial' : 'failed';\n\nconst evidence = usable.map((row) => ({\n  video_id: row.video_id,\n  source_url: row.source_url,\n  source_title: row.source_title,\n  author_name: row.author_name,\n  thumbnail_url: row.thumbnail_url,\n  analysis: row.analysis,\n}));\n\nconst conceptPrompt = [\n  'Create exactly three original YouTube thumbnail concepts for the planned video below.',\n  'Use only the supplied competitor analyses as design evidence.',\n  'Do not copy a competitor composition, wording, branding, face, artwork, or other protected expression.',\n  'Do not claim or predict CTR. Treat the analysis as creative evidence, not performance proof.',\n  '',\n  'PLANNED VIDEO:',\n  JSON.stringify({\n    title: brief.planned_video_title,\n    promise: brief.video_promise,\n    audience: brief.audience,\n    target_keyword: brief.target_keyword,\n    brand_palette: brief.brand_palette,\n    optional_thumbnail_text: brief.thumbnail_text,\n    aspect_ratio: brief.aspect_ratio,\n  }),\n  '',\n  'COMPETITOR EVIDENCE:',\n  JSON.stringify(evidence),\n  '',\n  'Each concept must explain which source video IDs informed it, why it is distinct, and which copy risks to avoid.',\n  'Prompts must be production-ready for ThumbCanvas, contain one focal point, and request no logos or copied artwork.',\n].join('\\n');\n\nreturn [{\n  json: {\n    planned_video_title: brief.planned_video_title,\n    video_promise: brief.video_promise,\n    audience: brief.audience,\n    target_keyword: brief.target_keyword,\n    brand_palette: brief.brand_palette,\n    thumbnail_text: brief.thumbnail_text,\n    aspect_ratio: brief.aspect_ratio,\n    submitted_at: brief.submitted_at,\n    workflow_version: brief.workflow_version,\n    openai_model: brief.openai_model,\n    thumbcanvas_base_url: brief.thumbcanvas_base_url,\n    analyses: rows,\n    usable_evidence: evidence,\n    successful_analysis_count: usable.length,\n    analysis_status: analysisStatus,\n    has_usable_analysis: usable.length > 0,\n    concept_prompt: conceptPrompt,\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
      "name": "Has Usable Visual Evidence?",
      "type": "n8n-nodes-base.if",
      "position": [
        1000,
        -20
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "35353535-3535-4535-8535-353535353535",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.has_usable_analysis }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
      "name": "Generate Three Evidence-Linked Concepts",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "onError": "continueRegularOutput",
      "position": [
        1240,
        -120
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.openai_model }}"
        },
        "options": {
          "store": false,
          "maxTokens": 1800,
          "textFormat": {
            "textOptions": {
              "name": "thumbnail_concept_pack",
              "type": "json_schema",
              "schema": "{\"type\":\"object\",\"properties\":{\"patterns_summary\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"concepts\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"},\"evidence\":{\"type\":\"string\"},\"source_video_ids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"headline_text\":{\"type\":\"string\"},\"composition\":{\"type\":\"string\"},\"palette\":{\"type\":\"string\"},\"prompt\":{\"type\":\"string\"},\"risks\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"additionalProperties\":false,\"required\":[\"id\",\"name\",\"evidence\",\"source_video_ids\",\"headline_text\",\"composition\",\"palette\",\"prompt\",\"risks\"]}}},\"additionalProperties\":false,\"required\":[\"patterns_summary\",\"concepts\"]}",
              "strict": true,
              "verbosity": "medium",
              "description": "Three original, evidence-linked thumbnail directions"
            }
          },
          "temperature": 0.5
        },
        "resource": "text",
        "simplify": true,
        "operation": "response",
        "responses": {
          "values": [
            {
              "role": "system",
              "type": "text",
              "content": "You are a YouTube thumbnail art director. Produce original concepts grounded in supplied evidence, avoid copying protected expression, and never claim guaranteed performance. Return the required JSON schema."
            },
            {
              "role": "user",
              "type": "text",
              "content": "={{ $json.concept_prompt }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
      "name": "Parse Concepts and Build Review",
      "type": "n8n-nodes-base.code",
      "position": [
        1480,
        -120
      ],
      "parameters": {
        "jsCode": "const aggregate = $('Aggregate Competitor Evidence').first().json;\nconst output = Array.isArray($json.output) ? $json.output : [];\nlet structured = null;\n\nfor (const item of output) {\n  if (!item || item.type !== 'message' || !Array.isArray(item.content)) continue;\n  const textPart = item.content.find((part) => part && part.type === 'output_text');\n  if (!textPart) continue;\n  if (textPart.text && typeof textPart.text === 'object') {\n    structured = textPart.text;\n  } else if (typeof textPart.text === 'string') {\n    try {\n      structured = JSON.parse(textPart.text);\n    } catch (error) {\n      structured = null;\n    }\n  }\n}\n\nconst concepts = Array.isArray(structured?.concepts) ? structured.concepts : [];\nconst conceptsReady = concepts.length === 3 && concepts.every((concept, index) =>\n  Number(concept.id) === index + 1 &&\n  typeof concept.name === 'string' &&\n  typeof concept.evidence === 'string' &&\n  typeof concept.prompt === 'string' &&\n  Array.isArray(concept.source_video_ids) &&\n  Array.isArray(concept.risks)\n);\n\nconst escapeHtml = (value) => {\n  const chars = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' };\n  return String(value ?? '').replace(/[&<>\"']/g, (char) => chars[char]);\n};\n\nlet reviewHtml = '';\nif (conceptsReady) {\n  const sourceHtml = aggregate.usable_evidence.map((source) => [\n    '<article>',\n    '<p><img src=\"' + escapeHtml(source.thumbnail_url) + '\" width=\"360\" alt=\"Competitor thumbnail\"></p>',\n    '<p><strong>' + escapeHtml(source.source_title) + '</strong><br>',\n    escapeHtml(source.author_name) + ' · <a href=\"' + escapeHtml(source.source_url) + '\" target=\"_blank\">Open source video</a></p>',\n    '</article>',\n  ].join('')).join('<hr>');\n\n  const conceptHtml = concepts.map((concept) => [\n    '<article>',\n    '<h3>' + escapeHtml(concept.id) + '. ' + escapeHtml(concept.name) + '</h3>',\n    '<p><strong>Evidence:</strong> ' + escapeHtml(concept.evidence) + '</p>',\n    '<p><strong>On-image text:</strong> ' + escapeHtml(concept.headline_text || 'None') + '</p>',\n    '<p><strong>Composition:</strong> ' + escapeHtml(concept.composition) + '</p>',\n    '<p><strong>Palette:</strong> ' + escapeHtml(concept.palette) + '</p>',\n    '<p><strong>Source IDs:</strong> ' + escapeHtml(concept.source_video_ids.join(', ')) + '</p>',\n    '<p><strong>Risks to review:</strong> ' + escapeHtml(concept.risks.join('; ') || 'None noted') + '</p>',\n    '</article>',\n  ].join('')).join('<hr>');\n\n  reviewHtml = [\n    '<h2>Evidence reviewed</h2>',\n    '<p>Open the public sources and compare them with the structured observations. OpenAI vision can make mistakes, especially with small text; your approval is required.</p>',\n    sourceHtml,\n    '<h2>Three original directions</h2>',\n    '<p><strong>Shared pattern summary:</strong> ' + escapeHtml((structured.patterns_summary || []).join('; ')) + '</p>',\n    conceptHtml,\n    '<p>No ThumbCanvas link exists yet. The workflow creates one only after you approve a concept below.</p>',\n  ].join('');\n}\n\nreturn [{\n  json: {\n    ...aggregate,\n    concepts_ready: conceptsReady,\n    concepts,\n    patterns_summary: Array.isArray(structured?.patterns_summary) ? structured.patterns_summary : [],\n    review_html: reviewHtml,\n    concept_error: conceptsReady ? '' : String($json.error?.message || $json.error || 'OpenAI did not return exactly three schema-valid concepts').slice(0, 500),\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
      "name": "Concept Pack Is Ready?",
      "type": "n8n-nodes-base.if",
      "position": [
        1720,
        -120
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "36363636-3636-4636-8636-363636363636",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.concepts_ready }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "ffffffff-ffff-4fff-8fff-ffffffffffff",
      "name": "Human Review and Approval",
      "type": "n8n-nodes-base.form",
      "notes": "The execution waits up to seven days. ThumbCanvas and Google Sheets are not called until a human selects one concept.",
      "position": [
        1960,
        -220
      ],
      "parameters": {
        "options": {
          "formTitle": "Review evidence and approve one direction",
          "buttonLabel": "Save decision",
          "formDescription": "Compare the public sources and AI observations. Approval is a human editorial decision, not a performance prediction."
        },
        "limitType": "afterTimeInterval",
        "operation": "page",
        "defineForm": "fields",
        "formFields": {
          "values": [
            {
              "html": "={{ $json.review_html }}",
              "fieldType": "html",
              "elementName": "concept_review"
            },
            {
              "fieldName": "approval_decision",
              "fieldType": "radio",
              "fieldLabel": "Decision",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Approve concept 1"
                  },
                  {
                    "option": "Approve concept 2"
                  },
                  {
                    "option": "Approve concept 3"
                  },
                  {
                    "option": "Reject all concepts"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldName": "review_notes",
              "fieldType": "textarea",
              "fieldLabel": "Review notes (optional)",
              "placeholder": "Record text, rights, identity, brand, or composition changes to make in ThumbCanvas."
            }
          ]
        },
        "resumeUnit": "days",
        "resumeAmount": 7,
        "limitWaitTime": true
      },
      "typeVersion": 2.5
    },
    {
      "id": "10101010-1010-4010-8010-101010101010",
      "name": "Resolve Human Approval",
      "type": "n8n-nodes-base.code",
      "position": [
        2200,
        -220
      ],
      "parameters": {
        "jsCode": "const pack = $('Parse Concepts and Build Review').first().json;\nconst decision = String($json.approval_decision || '').trim();\nconst reviewNotes = String($json.review_notes || '').replace(/\\s+/g, ' ').trim().slice(0, 500);\nconst match = decision.match(/^Approve concept ([123])$/);\nconst approved = Boolean(match);\n\nif (!approved) {\n  return [{ json: {\n    approved: false,\n    decision: decision || 'Reject all concepts',\n    review_notes: reviewNotes,\n    planned_video_title: pack.planned_video_title,\n  } }];\n}\n\nconst concept = pack.concepts[Number(match[1]) - 1];\nconst query = [\n  ['prompt', concept.prompt],\n  ['aspect', pack.aspect_ratio],\n].map((entry) => encodeURIComponent(entry[0]) + '=' + encodeURIComponent(String(entry[1]))).join('&');\nconst thumbcanvasUrl = pack.thumbcanvas_base_url + '?' + query;\n\nreturn [{\n  json: {\n    approved: true,\n    approved_concept: concept,\n    review_notes: reviewNotes,\n    thumbcanvas_url: thumbcanvasUrl,\n    audit_row: {\n      approved_at: new Date().toISOString(),\n      planned_video_title: pack.planned_video_title,\n      target_keyword: pack.target_keyword,\n      audience: pack.audience,\n      competitor_urls: pack.usable_evidence.map((source) => source.source_url).join('\\n'),\n      approved_concept: concept.name,\n      concept_evidence: concept.evidence,\n      thumbcanvas_url: thumbcanvasUrl,\n      openai_model: pack.openai_model,\n      analysis_status: pack.analysis_status,\n      review_notes: reviewNotes,\n      workflow_version: pack.workflow_version,\n    },\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "12121212-1212-4212-8212-121212121212",
      "name": "A Concept Was Approved?",
      "type": "n8n-nodes-base.if",
      "position": [
        2440,
        -220
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "37373737-3737-4737-8737-373737373737",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.approved }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "13131313-1313-4313-8313-131313131313",
      "name": "Map Approved Brief to Audit Columns",
      "type": "n8n-nodes-base.set",
      "position": [
        2680,
        -320
      ],
      "parameters": {
        "mode": "manual",
        "options": {
          "dotNotation": true
        },
        "assignments": {
          "assignments": [
            {
              "id": "38383838-3838-4838-8838-383838383838",
              "name": "approved_at",
              "type": "string",
              "value": "={{ $json.audit_row.approved_at }}"
            },
            {
              "id": "39393939-3939-4939-8939-393939393939",
              "name": "planned_video_title",
              "type": "string",
              "value": "={{ $json.audit_row.planned_video_title }}"
            },
            {
              "id": "40404040-4040-4040-8040-404040404040",
              "name": "target_keyword",
              "type": "string",
              "value": "={{ $json.audit_row.target_keyword }}"
            },
            {
              "id": "41414141-4141-4141-8141-414141414141",
              "name": "audience",
              "type": "string",
              "value": "={{ $json.audit_row.audience }}"
            },
            {
              "id": "42424242-4242-4242-8242-424242424242",
              "name": "competitor_urls",
              "type": "string",
              "value": "={{ $json.audit_row.competitor_urls }}"
            },
            {
              "id": "43434343-4343-4343-8343-434343434343",
              "name": "approved_concept",
              "type": "string",
              "value": "={{ $json.audit_row.approved_concept }}"
            },
            {
              "id": "45454545-4545-4545-8545-454545454545",
              "name": "concept_evidence",
              "type": "string",
              "value": "={{ $json.audit_row.concept_evidence }}"
            },
            {
              "id": "46464646-4646-4646-8646-464646464646",
              "name": "thumbcanvas_url",
              "type": "string",
              "value": "={{ $json.audit_row.thumbcanvas_url }}"
            },
            {
              "id": "47474747-4747-4747-8747-474747474747",
              "name": "openai_model",
              "type": "string",
              "value": "={{ $json.audit_row.openai_model }}"
            },
            {
              "id": "48484848-4848-4848-8848-484848484848",
              "name": "analysis_status",
              "type": "string",
              "value": "={{ $json.audit_row.analysis_status }}"
            },
            {
              "id": "49494949-4949-4949-8949-494949494949",
              "name": "review_notes",
              "type": "string",
              "value": "={{ $json.audit_row.review_notes }}"
            },
            {
              "id": "50505050-5050-4050-8050-505050505050",
              "name": "workflow_version",
              "type": "string",
              "value": "={{ $json.audit_row.workflow_version }}"
            }
          ]
        },
        "includeOtherFields": false
      },
      "typeVersion": 3.5
    },
    {
      "id": "14141414-1414-4414-8414-141414141414",
      "name": "Append Approved Brief to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Setup required: connect Google Sheets OAuth, choose a spreadsheet, and choose or create a sheet. Recommended header names match the preceding Edit Fields node.",
      "onError": "continueRegularOutput",
      "position": [
        2920,
        -320
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData"
        },
        "options": {
          "useAppend": true,
          "handlingExtraData": "insertInNewColumn"
        },
        "resource": "sheet",
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "thumbnail_audit"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "Choose a competitor-analysis spreadsheet"
        },
        "authentication": "oAuth2"
      },
      "typeVersion": 4.7
    },
    {
      "id": "15151515-1515-4515-8515-151515151515",
      "name": "Verify Audit Write Result",
      "type": "n8n-nodes-base.code",
      "position": [
        3160,
        -320
      ],
      "parameters": {
        "jsCode": "const approval = $('Resolve Human Approval').first().json;\nconst errorMessage = String($json.error?.message || $json.error || '').slice(0, 500);\nreturn [{\n  json: {\n    ...approval,\n    sheet_write_ok: !errorMessage,\n    sheet_error: errorMessage,\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "16161616-1616-4616-8616-161616161616",
      "name": "Audit Row Was Saved?",
      "type": "n8n-nodes-base.if",
      "position": [
        3400,
        -320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "51515151-5151-4151-8151-515151515151",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.sheet_write_ok }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "17171717-1717-4717-8717-171717171717",
      "name": "Show Saved ThumbCanvas Handoff",
      "type": "n8n-nodes-base.form",
      "position": [
        3640,
        -420
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "showText",
        "responseText": "={{ '<h1>Approved brief saved</h1><p>Your selected direction was appended to Google Sheets.</p><p><a href=\"' + $json.thumbcanvas_url + '\" target=\"_blank\">Open the approved direction in ThumbCanvas</a></p><p>Review every generated image for text, identity, rights, and brand fit before publishing.</p>' }}"
      },
      "typeVersion": 2.5
    },
    {
      "id": "18181818-1818-4818-8818-181818181818",
      "name": "Show Rejected Decision",
      "type": "n8n-nodes-base.form",
      "position": [
        2680,
        -80
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "text",
        "completionTitle": "No concept was approved",
        "completionMessage": "The review decision was recorded in this execution. No ThumbCanvas link was created and nothing was written to Google Sheets."
      },
      "typeVersion": 2.5
    },
    {
      "id": "19191919-1919-4919-8919-191919191919",
      "name": "Show Input Corrections",
      "type": "n8n-nodes-base.form",
      "position": [
        -680,
        300
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "showText",
        "responseText": "={{ '<h1>Check the brief</h1><p>' + $json.errors.join('</p><p>') + '</p><p>Return to the first form page and submit at least two unique public YouTube URLs.</p>' }}"
      },
      "typeVersion": 2.5
    },
    {
      "id": "20202020-2020-4020-8020-202020202020",
      "name": "Show Evidence Failure",
      "type": "n8n-nodes-base.form",
      "position": [
        1240,
        180
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "text",
        "completionTitle": "No visual evidence was available",
        "completionMessage": "OpenAI did not return a usable visual analysis for any submitted thumbnail. Check that the videos are public, the thumbnail URLs are reachable, and the OpenAI credential has access to the selected vision-capable model."
      },
      "typeVersion": 2.5
    },
    {
      "id": "21212121-2121-4121-8121-212121212121",
      "name": "Show Concept Generation Failure",
      "type": "n8n-nodes-base.form",
      "position": [
        1960,
        40
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "showText",
        "responseText": "={{ '<h1>Concept generation needs attention</h1><p>' + $json.concept_error + '</p><p>No approval, ThumbCanvas link, or Google Sheets row was created.</p>' }}"
      },
      "typeVersion": 2.5
    },
    {
      "id": "23232323-2323-4323-8323-232323232323",
      "name": "Show Sheets Write Failure",
      "type": "n8n-nodes-base.form",
      "position": [
        3640,
        -180
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "showText",
        "responseText": "={{ '<h1>Approved, but the audit row was not saved</h1><p>' + $json.sheet_error + '</p><p><a href=\"' + $json.thumbcanvas_url + '\" target=\"_blank\">Open the approved direction in ThumbCanvas</a></p><p>Fix the Google Sheets credential or destination, then rerun the workflow so the approval is auditable.</p>' }}"
      },
      "typeVersion": 2.5
    },
    {
      "id": "24242424-2424-4424-8424-242424242424",
      "name": "Overview Setup and Safety",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2300,
        -720
      ],
      "parameters": {
        "color": 4,
        "width": 820,
        "height": 1480,
        "content": "## Analyze competitor YouTube thumbnails with OpenAI and save approved briefs to Google Sheets\n\n### Who this is for\nYouTube creators, thumbnail designers, and content teams who want evidence-informed creative directions without copying competitors or pretending that visual analysis guarantees click-through rate.\n\n### What it does\nThis workflow collects a planned-video brief plus two or three public competitor YouTube URLs. It validates and deduplicates the links, retrieves public oEmbed metadata, and sends each public thumbnail to OpenAI for a schema-validated visual analysis. It then aggregates the usable evidence and produces exactly three original thumbnail concepts.\n\nA human review page shows the public sources, the analysis, and all three concepts. Only an approved concept becomes a pre-filled [ThumbCanvas](https://thumbcanvas.com/create-thumbs) link. The approved decision is then appended to Google Sheets as an audit row. Rejected concepts create neither a ThumbCanvas link nor a sheet row.\n\n### Setup\n1. Add an OpenAI credential to both OpenAI nodes. The default model is `gpt-4.1-mini`; change it in **Set Template Configuration** if your account uses another vision-capable model.\n2. Connect Google Sheets OAuth in **Append Approved Brief to Google Sheets**. Select a spreadsheet and a sheet. Use these header names: `approved_at`, `planned_video_title`, `target_keyword`, `audience`, `competitor_urls`, `approved_concept`, `concept_evidence`, `thumbcanvas_url`, `openai_model`, `analysis_status`, `review_notes`, `workflow_version`.\n3. Publish the workflow, open the production form URL, and submit a brief.\n\n### Important boundaries\nOpenAI vision can misread small text or visual details, so the workflow exposes sources and limitations for human checking. It never predicts CTR. Enter only public competitor links and non-confidential brief data. OpenAI receives the thumbnail URL and brief context; Google Sheets stores only the approved row. n8n may retain execution data according to your instance settings.\n\nThumbCanvas generation happens only when the reviewer opens the final link. No image is auto-published."
      },
      "typeVersion": 1
    },
    {
      "id": "25252525-2525-4525-8525-252525252525",
      "name": "Step 1 Input and Configuration",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1440,
        -640
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 520,
        "content": "## 1. Collect and validate\n\nThe n8n Form Trigger asks for the planned video, audience, keyword, palette, optional thumbnail text, and two or three public competitor URLs.\n\n**Normalize and Validate Brief** accepts HTTPS YouTube watch, youtu.be, Shorts, and embed links; extracts strict 11-character video IDs; removes duplicates; and rejects packs with fewer than two unique videos. Optional thumbnail text is limited to four words.\n\nUse **Set Template Configuration** to change the OpenAI model or ThumbCanvas base URL in one place. Do not place API keys in fields or Code nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "26262626-2626-4626-8626-262626262626",
      "name": "Step 2 Public Evidence and Vision",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -460,
        -700
      ],
      "parameters": {
        "color": 7,
        "width": 1160,
        "height": 560,
        "content": "## 2. Gather visible evidence\n\nEach competitor becomes one n8n item. The HTTP Request node reads public YouTube oEmbed metadata; the OpenAI node receives the public thumbnail URL at high image detail and returns a strict visual-analysis schema.\n\nThe analysis covers focal subject, visible text, composition, contrast, emotion, hook, transferable patterns, copy risks, confidence, and limitations. It deliberately excludes performance and CTR claims.\n\nFailures continue as labeled items so the aggregate can report `complete`, `partial`, or `failed` evidence instead of silently dropping a source."
      },
      "typeVersion": 1
    },
    {
      "id": "27272727-2727-4727-8727-272727272727",
      "name": "Step 3 Concepts and Human Gate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1160,
        -780
      ],
      "parameters": {
        "color": 7,
        "width": 1120,
        "height": 560,
        "content": "## 3. Generate and review\n\nThe second OpenAI call receives the planned-video brief and validated visual evidence. Strict structured output requires exactly three original directions, each with source video IDs, rationale, composition, palette, production prompt, and risks.\n\nThe workflow then pauses for up to seven days on an n8n Form review page. The reviewer can open the public source videos, compare evidence, approve one concept, add notes, or reject all three.\n\nThis is the editorial gate: no approval means no ThumbCanvas link and no Google Sheets write."
      },
      "typeVersion": 1
    },
    {
      "id": "28282828-2828-4828-8828-282828282828",
      "name": "Step 4 ThumbCanvas and Audit",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2480,
        -840
      ],
      "parameters": {
        "color": 7,
        "width": 1180,
        "height": 580,
        "content": "## 4. Create the handoff and audit it\n\nAfter approval, **Resolve Human Approval** encodes only the chosen production prompt and 16:9 aspect ratio into a ThumbCanvas create URL. Opening that URL is a separate human action and may use the reviewer's ThumbCanvas plan or credits.\n\n**Map Approved Brief to Audit Columns** creates a predictable row shape. Google Sheets auto-maps those fields to matching headers. A final check separates successful writes from credential or destination failures.\n\nIf the sheet write fails, the workflow keeps the approved ThumbCanvas link visible but clearly labels the audit failure. Fix the Sheets setup and rerun; do not treat an unsaved approval as fully completed."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Brief Is Valid?": {
      "main": [
        [
          {
            "node": "Set Template Configuration",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Show Input Corrections",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Audit Row Was Saved?": {
      "main": [
        [
          {
            "node": "Show Saved ThumbCanvas Handoff",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Show Sheets Write Failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Concept Pack Is Ready?": {
      "main": [
        [
          {
            "node": "Human Review and Approval",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Show Concept Generation Failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Human Approval": {
      "main": [
        [
          {
            "node": "A Concept Was Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "A Concept Was Approved?": {
      "main": [
        [
          {
            "node": "Map Approved Brief to Audit Columns",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Show Rejected Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Competitor Jobs": {
      "main": [
        [
          {
            "node": "Fetch Public YouTube Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Human Review and Approval": {
      "main": [
        [
          {
            "node": "Resolve Human Approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Audit Write Result": {
      "main": [
        [
          {
            "node": "Audit Row Was Saved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Template Configuration": {
      "main": [
        [
          {
            "node": "Prepare Competitor Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Usable Visual Evidence?": {
      "main": [
        [
          {
            "node": "Generate Three Evidence-Linked Concepts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Show Evidence Failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize and Validate Brief": {
      "main": [
        [
          {
            "node": "Brief Is Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Competitor Evidence": {
      "main": [
        [
          {
            "node": "Has Usable Visual Evidence?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Public YouTube Metadata": {
      "main": [
        [
          {
            "node": "Combine Public YouTube Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Public YouTube Metadata": {
      "main": [
        [
          {
            "node": "Analyze Each Thumbnail with OpenAI Vision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Concepts and Build Review": {
      "main": [
        [
          {
            "node": "Concept Pack Is Ready?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect Video Brief and Competitors": {
      "main": [
        [
          {
            "node": "Normalize and Validate Brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Approved Brief to Audit Columns": {
      "main": [
        [
          {
            "node": "Append Approved Brief to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Structured Visual Analysis": {
      "main": [
        [
          {
            "node": "Aggregate Competitor Evidence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Approved Brief to Google Sheets": {
      "main": [
        [
          {
            "node": "Verify Audit Write Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Three Evidence-Linked Concepts": {
      "main": [
        [
          {
            "node": "Parse Concepts and Build Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Each Thumbnail with OpenAI Vision": {
      "main": [
        [
          {
            "node": "Validate Structured Visual Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}
