{
  "id": "DemoApparelWf001",
  "name": "01 · Brand voice drafts (Demo Apparel Co.)",
  "nodes": [
    {
      "parameters": {
        "content": "## 1 · Brand kit\nVoice rules, banned phrases, segments and CTAs live in a Google Sheet the owner can edit. Workflow 2 appends new rules here.",
        "height": 550,
        "width": 680,
        "color": 5
      },
      "id": "bca27ad3-390d-4db1-8c7f-42a67fd36018",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -50,
        50
      ]
    },
    {
      "parameters": {
        "content": "## 2 · Draft with Claude\nEach new content source (story, partner launch, resource) becomes drafts for Instagram, Reels/TikTok, email and blog, per audience segment.",
        "height": 550,
        "width": 920,
        "color": 6
      },
      "id": "27fe5328-7970-4c5b-a514-e9facd99b12c",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        670,
        50
      ]
    },
    {
      "parameters": {
        "content": "## 3 · Rule check, then human review\nCode checks banned phrases, length per channel and CTA. Nothing is published here: rows land as \"needs review\".",
        "height": 550,
        "width": 440,
        "color": 4
      },
      "id": "6ccc39df-58de-4286-b4d9-4a5b4cd297d3",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1630,
        50
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "50ab6796-0a59-4200-a535-c903dec27bf4",
      "name": "Every Monday 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.4,
      "position": [
        0,
        190
      ]
    },
    {
      "parameters": {},
      "id": "bc0802ae-7948-4720-b09a-21e82f1b1468",
      "name": "Run manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        410
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/REPLACE_WITH_BRAND_KIT_SHEET_ID/edit"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Brand kit"
        },
        "options": {}
      },
      "id": "658fc913-58ed-48fd-9af3-d5d8a6a8133d",
      "name": "Load brand kit",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        240,
        300
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "credGSheets00001",
          "name": "Google Sheets (client)"
        }
      },
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "// \"Brand kit\" sheet: one row per rule. Columns: type | value | channel | notes\n// type = voice_rule | banned_phrase | segment | required_cta | max_length\nconst kit = { brand: 'Demo Apparel Co.', voice: [], banned: [], segments: [], ctas: [], maxLength: {} };\nfor (const { json: r } of $input.all()) {\n  const type = String(r.type || '').trim().toLowerCase();\n  const value = String(r.value || '').trim();\n  if (!value) continue;\n  if (type === 'voice_rule') kit.voice.push(value);\n  else if (type === 'banned_phrase') kit.banned.push(value);\n  else if (type === 'segment') kit.segments.push(value);\n  else if (type === 'required_cta') kit.ctas.push(value);\n  else if (type === 'max_length' && r.channel) kit.maxLength[String(r.channel).trim().toLowerCase()] = Number(value);\n}\n// Safe defaults so a half-filled sheet still works\nif (!kit.segments.length) kit.segments = ['active customers', 'gift buyers', 'coaches & teams', 'community'];\nkit.maxLength = { instagram: 2200, tiktok_script: 900, email_block: 1200, blog_intro: 700, ...kit.maxLength };\nreturn [{ json: kit }];"
      },
      "id": "e0b944bb-1d13-4a5d-a74b-615138b329f0",
      "name": "Compile brand rules",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        300
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/REPLACE_WITH_CONTENT_CALENDAR_SHEET_ID/edit"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Content sources"
        },
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "status",
              "lookupValue": "new"
            }
          ]
        }
      },
      "id": "75881fb2-4933-4ac3-8509-c2859f151e6b",
      "name": "Read new content sources",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        720,
        300
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "credGSheets00001",
          "name": "Google Sheets (client)"
        }
      },
      "executeOnce": true
    },
    {
      "parameters": {
        "jsCode": "// One prompt per content source (story / partner launch / resource)\nconst kit = $('Compile brand rules').first().json;\nconst src = $json; // columns: source_id | type | title | details | link | status\n\nconst system = [\n  `You write social and email copy for ${kit.brand}, a small apparel and lifestyle brand.`,\n  'Voice rules:',\n  ...kit.voice.map((v) => '- ' + v),\n  'Never use these phrases: ' + (kit.banned.join('; ') || 'none listed'),\n  'End every draft with one of these calls to action: ' + (kit.ctas.join(' | ') || 'Shop the drop'),\n  'Return ONLY valid JSON. No prose, no markdown fences.',\n].join('\\n');\n\nconst user = [\n  `Content source (${src.type}): ${src.title}`,\n  String(src.details || ''),\n  'Link: ' + (src.link || 'none'),\n  '',\n  'Write one draft per channel for each audience segment: ' + kit.segments.join(', ') + '.',\n  'Channels (max characters): ' + Object.entries(kit.maxLength).map(([c, n]) => `${c} (${n})`).join(', ') + '.',\n  'tiktok_script = a 20-30 second Reels/TikTok script with numbered shots.',\n  'JSON shape: {\"drafts\":[{\"channel\":\"instagram|tiktok_script|email_block|blog_intro\",\"segment\":\"...\",\"text\":\"...\",\"cta\":\"...\"}]}',\n].join('\\n');\n\nreturn { json: { source_id: src.source_id, source_title: src.title, system_prompt: system, user_prompt: user } };",
        "mode": "runOnceForEachItem"
      },
      "id": "f6f06fc9-7efd-40a3-a060-15779b085048",
      "name": "Build Claude prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  model: 'claude-sonnet-5-5',\n  max_tokens: 8000,\n  system: $json.system_prompt,\n  messages: [{ role: 'user', content: $json.user_prompt }]\n}) }}",
        "options": {
          "timeout": 180000
        }
      },
      "id": "9b53bb52-0fdf-4d30-ae8f-e8bf17280430",
      "name": "Draft posts with Claude",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        1200,
        300
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "credAnthropic001",
          "name": "Anthropic API"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Claude replies with content blocks; take the text block and parse it as JSON.\nfunction claudeJson(resp) {\n  const block = (resp.content || []).find((b) => b.type === 'text');\n  if (!block) throw new Error('Claude returned no text (stop_reason: ' + resp.stop_reason + ')');\n  const raw = block.text.trim().replace(/^```(?:json)?\\s*/i, '').replace(/```$/, '').trim();\n  return JSON.parse(raw);\n}\n\n// One output item per draft, carrying its content source.\nconst out = [];\n$input.all().forEach((item, i) => {\n  const src = $('Build Claude prompt').itemMatching(i).json;\n  for (const d of claudeJson(item.json).drafts || []) {\n    out.push({ json: { source_id: src.source_id, source_title: src.source_title, ...d } });\n  }\n});\nreturn out;"
      },
      "id": "66d6b286-d57c-4295-9595-db1aa5597674",
      "name": "Split into drafts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Flag (never silently fix) anything that breaks the brand kit.\nconst kit = $('Compile brand rules').first().json;\nconst lower = (s) => String(s || '').toLowerCase();\nconst stamp = Date.now().toString(36);\n\nreturn $input.all().map(({ json: d }, i) => {\n  const text = String(d.text || '');\n  const flags = [];\n  for (const phrase of kit.banned) {\n    if (lower(text).includes(lower(phrase))) flags.push(`banned phrase \"${phrase}\"`);\n  }\n  const max = kit.maxLength[d.channel];\n  if (max && text.length > max) flags.push(`too long for ${d.channel} (${text.length}/${max})`);\n  if (kit.ctas.length && !kit.ctas.some((c) => lower(text).includes(lower(c)))) flags.push('missing required CTA');\n  if (!kit.segments.includes(d.segment)) flags.push(`unknown segment \"${d.segment}\"`);\n\n  return {\n    json: {\n      draft_id: `d-${stamp}-${i}`,\n      created_at: $now.toISO(),\n      planned_date: '',\n      source_id: d.source_id,\n      source: d.source_title,\n      channel: d.channel,\n      segment: d.segment,\n      text,\n      cta: d.cta || '',\n      image_url: '',\n      rule_check: flags.length ? 'FLAGGED' : 'OK',\n      rule_flags: flags.join(' | '),\n      status: 'needs review',\n    },\n  };\n});"
      },
      "id": "18550f61-17c3-4afe-bf76-5b670a24535b",
      "name": "Check drafts against brand rules",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1680,
        300
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/REPLACE_WITH_CONTENT_CALENDAR_SHEET_ID/edit"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Content calendar"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "id": "4da9573d-8a32-4511-b988-aca7ad251334",
      "name": "Append to Content calendar",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1920,
        300
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "credGSheets00001",
          "name": "Google Sheets (client)"
        }
      }
    }
  ],
  "connections": {
    "Every Monday 08:00": {
      "main": [
        [
          {
            "node": "Load brand kit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run manually": {
      "main": [
        [
          {
            "node": "Load brand kit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load brand kit": {
      "main": [
        [
          {
            "node": "Compile brand rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compile brand rules": {
      "main": [
        [
          {
            "node": "Read new content sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read new content sources": {
      "main": [
        [
          {
            "node": "Build Claude prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Claude prompt": {
      "main": [
        [
          {
            "node": "Draft posts with Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft posts with Claude": {
      "main": [
        [
          {
            "node": "Split into drafts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split into drafts": {
      "main": [
        [
          {
            "node": "Check drafts against brand rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check drafts against brand rules": {
      "main": [
        [
          {
            "node": "Append to Content calendar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
