{
  "name": "Brand Checker",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "brand-check",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "69b6765b-64ce-42ba-81ae-97315c6ddab2",
      "name": "Check a draft",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "f4701e4a-914e-4680-932f-3aee94cef494"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtDay": [
                0
              ],
              "triggerAtHour": 18,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "id": "e83a2bf6-cbdb-4682-add0-533e960d1391",
      "name": "Every Sunday at 6 PM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        400
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "c1ec654f-0574-4048-b86e-cee1442a02c3",
              "name": "brand_kit_doc_url",
              "value": "PASTE THE LINK TO YOUR BRAND KIT GOOGLE DOC HERE",
              "type": "string"
            },
            {
              "id": "1b40de4b-d7e8-49b0-95e1-84e1cdc49203",
              "name": "brand_log_sheet_url",
              "value": "PASTE THE LINK TO YOUR BRAND LOG GOOGLE SHEET HERE",
              "type": "string"
            },
            {
              "id": "eab5b020-0f67-42fb-b144-454bc170c55c",
              "name": "report_email",
              "value": "you@example.com",
              "type": "string"
            },
            {
              "id": "a8bcac6c-3a8f-4e81-9ae1-f4a47e2c0c82",
              "name": "model",
              "value": "claude-sonnet-5-5",
              "type": "string"
            },
            {
              "id": "38dd6605-762e-4fde-ac79-6f7fb4e52105",
              "name": "pass_score",
              "value": 80,
              "type": "number"
            },
            {
              "id": "4078f901-6baf-4b53-80d4-b31a747ae56b",
              "name": "price_input_per_million",
              "value": 2,
              "type": "number"
            },
            {
              "id": "e1827e49-9bb9-44c7-b89f-31bcb32ce858",
              "name": "price_output_per_million",
              "value": 10,
              "type": "number"
            },
            {
              "id": "842a4990-9184-4c84-8083-d198548272df",
              "name": "mode",
              "value": "={{ $json.headers ? 'check' : 'report' }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "1c4df9eb-95d4-4e4a-8393-751ef588fa59",
      "name": "1. Your settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        200
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "2efcdb39-437e-4736-b9bf-4e3ad7f663e9",
              "leftValue": "={{ $json.mode }}",
              "rightValue": "check",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "100d8e32-d648-4c03-a295-3694662b79dc",
      "name": "Is it a check?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        500,
        200
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "43f94d5d-fcb8-4981-a514-07bc7934fda9",
              "name": "checker_rules",
              "value": "You are the Brand Checker. You work for one person or one small business.\nBefore anything is published, you check the draft against their BRAND KIT and decide if it can ship.\nYou are strict and fair. You never praise to be nice. You never invent facts.\n\nWHAT YOU GET\n- BRAND KIT: their rules, proof, pillars, voice, lens and checker rules.\n- PLATFORM: where the draft will be posted.\n- DRAFT: the words to check.\n\nHOW TO CHECK, IN THIS ORDER\n1. HARD FAILS FIRST. Look for each one. Quote the exact words that cause it.\n   a) A number, result, client or credential that is not in the Proof Bank of the kit. If the kit has no Proof Bank, any specific result claim counts.\n   b) A claim that contradicts the Proof Bank or anything else in the kit.\n   c) Anything on the kit's must-never-ship list (from the Specialty Lens).\n   d) A banned word from the kit.\n   e) Placeholder text left in: \"Your Name\", \"[link]\", \"TBD\", \"XXX\", \"lorem ipsum\", empty brackets.\n   f) The draft fits none of the kit's 3 content pillars.\n2. THEN SCORE FOUR PARTS.\n   - proof (0 to 30): is there a real, specific number or receipt from the Proof Bank? Is every claim backed?\n   - voice (0 to 25): does it open the way the kit says? Short sentences, plain words, the kit's tone and punctuation rules?\n   - positioning (0 to 25): does it fit one pillar, show one belief, speak to the kit's audience?\n   - format (0 to 20): does it follow the kit's rule for this PLATFORM (length, first line, links, hashtags, ending)? If the kit has no rule for this platform, judge it by common sense for that platform.\n   score = proof + voice + positioning + format.\n   Scoring anchors: 90+ only when you would publish it as is. 80 to 89 is good with small fixes. 60 to 79 needs real work. Under 60 is off-brand.\n3. FLAGS. List up to 6 problems, most important first. For each: the exact words from the draft, the kit rule it breaks, and a concrete fix.\n4. FIXED DRAFT. If the result is FAIL, rewrite the draft so it would pass.\n   - Keep the author's meaning and their own facts.\n   - You may cut, reorder and reword. You may NOT add any new number, result, client, date or claim.\n   - Where a real number is needed, write [ADD PROOF: what kind of number] so the author fills it from real data.\n   - Follow the platform rule. No emojis unless the kit allows them. No em dashes.\n   If the result is PASS, return the draft unchanged in fixed_draft.\n\nTHE RESULT\n- PASS only if score is 80 or more AND hard_fails is empty.\n- Otherwise FAIL.\n\nRETURN ONLY THIS JSON, NOTHING BEFORE OR AFTER IT:\n{\n  \"score\": 0,\n  \"parts\": { \"proof\": 0, \"voice\": 0, \"positioning\": 0, \"format\": 0 },\n  \"result\": \"PASS or FAIL\",\n  \"hard_fails\": [ \"short name: the exact words\" ],\n  \"pillar\": \"the pillar name it fits, or NONE\",\n  \"flags\": [ { \"quote\": \"exact words from the draft\", \"rule\": \"the kit rule it breaks\", \"fix\": \"what to write instead\" } ],\n  \"fixed_draft\": \"the full fixed draft\",\n  \"one_line\": \"the verdict in 15 words or fewer\"\n}\n",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "998d5b3d-689d-4044-9286-2e08008aa350",
      "name": "2. Checker rules",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        760,
        0
      ]
    },
    {
      "parameters": {
        "operation": "get",
        "documentURL": "={{ $('1. Your settings').first().json.brand_kit_doc_url }}"
      },
      "id": "fee8cd20-8035-40f4-9aba-5a051cccc990",
      "name": "3. Get my Brand Kit",
      "type": "n8n-nodes-base.googleDocs",
      "typeVersion": 2,
      "position": [
        1000,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Box 4: puts your Brand Kit, the platform and the draft into one question for Claude.\n// You do not need to change anything in this box.\nconst s = $('1. Your settings').first().json;\nconst rules = String($('2. Checker rules').first().json.checker_rules || '');\nconst kit = String($input.first().json.content || '').trim();\n\n// The page sends { platform, draft }. Some setups deliver it as text, so we unpack it safely.\nlet body = s.body;\nif (typeof body === 'string') {\n  try { body = JSON.parse(body); } catch (e) { body = { draft: body }; }\n}\nif (body && typeof body === 'object' && body.draft === undefined) {\n  const keys = Object.keys(body);\n  if (keys.length === 1 && keys[0].trim().startsWith('{')) {\n    try { body = JSON.parse(keys[0]); } catch (e) { /* keep as is */ }\n  }\n}\nbody = body || {};\n\nconst platform = String(body.platform || 'Post').trim().slice(0, 60) || 'Post';\nconst draft = String(body.draft || '').trim().slice(0, 8000);\n\nif (!draft) {\n  return [{ json: { skip: true, error: 'The draft was empty. Paste your post and press Check again.', platform, draft } }];\n}\nif (kit.length < 200) {\n  return [{ json: { skip: true, error: 'Your Brand Kit doc looks empty. Paste your Brand Kit into the Google Doc, then try again.', platform, draft } }];\n}\n\nconst today = $now.toFormat('yyyy-LL-dd');\n\n// Structured output: Claude must answer in exactly this shape, so the reply is always valid JSON.\nconst str = { type: 'string' };\nconst int = { type: 'integer' };\nconst schema = {\n  type: 'object',\n  properties: {\n    score: int,\n    parts: {\n      type: 'object',\n      properties: { proof: int, voice: int, positioning: int, format: int },\n      required: ['proof', 'voice', 'positioning', 'format'],\n      additionalProperties: false,\n    },\n    result: { type: 'string', enum: ['PASS', 'FAIL'] },\n    hard_fails: { type: 'array', items: str },\n    pillar: str,\n    flags: {\n      type: 'array',\n      items: {\n        type: 'object',\n        properties: { quote: str, rule: str, fix: str },\n        required: ['quote', 'rule', 'fix'],\n        additionalProperties: false,\n      },\n    },\n    fixed_draft: str,\n    one_line: str,\n  },\n  required: ['score', 'parts', 'result', 'hard_fails', 'pillar', 'flags', 'fixed_draft', 'one_line'],\n  additionalProperties: false,\n};\n\nconst request = {\n  model: s.model,\n  max_tokens: 4000,\n  system: rules,\n  output_config: { format: { type: 'json_schema', schema } },\n  messages: [\n    {\n      role: 'user',\n      content: `TODAY: ${today}\\n\\nBRAND KIT:\\n${kit}\\n\\nPLATFORM: ${platform}\\n\\nDRAFT:\\n${draft}`,\n    },\n  ],\n};\n\nreturn [{ json: { skip: false, platform, draft, request } }];\n"
      },
      "id": "f322ede6-53b4-4e3f-a22c-98458b4631a8",
      "name": "4. Build the question",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1240,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "anthropicApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.request) }}",
        "options": {
          "timeout": 90000
        }
      },
      "id": "b868f82d-7f37-46c7-b57c-82c8f60f41ec",
      "name": "5. Ask Claude",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1480,
        0
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Box 6: reads Claude's answer, checks the maths, and decides PASS or FAIL by your rule.\n// You do not need to change anything in this box.\nconst s = $('1. Your settings').first().json;\nconst build = $('4. Build the question').first().json;\nconst res = $input.first().json || {};\nconst passScore = Number(s.pass_score) || 80;\nconst priceIn = Number(s.price_input_per_million) || 0;\nconst priceOut = Number(s.price_output_per_million) || 0;\nconst stamp = $now.toFormat('yyyy-LL-dd HH:mm');\n\nfunction finish(response, extra) {\n  const log = {\n    Date: stamp,\n    Platform: build.platform || '',\n    Score: response.ok ? String(response.score) : '',\n    Result: response.ok ? response.result : 'ERROR',\n    'Hard fails': response.ok ? response.hard_fails.join(' | ') : '',\n    Flags: response.ok ? String(response.flags.length) : '',\n    'Top flag': response.ok && response.flags[0] ? response.flags[0].rule : (response.error || ''),\n    Pillar: response.ok ? response.pillar : '',\n    'Cost USD': extra && extra.cost !== undefined ? extra.cost.toFixed(4) : '',\n    Draft: String(build.draft || '').slice(0, 1500),\n    'Fixed draft': response.ok ? String(response.fixed_draft || '').slice(0, 1500) : '',\n  };\n  return [{ json: { response, log } }];\n}\n\nfunction fail(message) {\n  return finish({ ok: false, error: message });\n}\n\nif (build.skip) return fail(build.error);\n\n// n8n wraps Anthropic's errors and often hides the real reason behind \"Bad request\".\n// This digs the real reason out, then says in plain words what to do.\nfunction realReason(err) {\n  if (!err) return '';\n  if (typeof err === 'string') return err;\n  const bits = [];\n  const add = (v) => { if (typeof v === 'string' && v.trim()) bits.push(v); };\n  add(err.description);\n  if (err.context && err.context.data && err.context.data.error) add(err.context.data.error.message);\n  if (Array.isArray(err.messages)) err.messages.forEach(add);\n  add(err.message);\n  if (err.cause) add(err.cause.message);\n  for (const b of bits) {\n    const m = b.match(/message\\\\*\"\\s*:\\s*\\\\*\"([^\"\\\\]+)/);\n    if (m) return m[1];\n  }\n  const plain = bits.find((b) => !/^(Bad request|Authorization failed|The resource you are requesting|Forbidden|The service)/i.test(b));\n  return plain || bits[0] || '';\n}\nfunction explain(err) {\n  const why = realReason(err);\n  const all = why + ' ' + JSON.stringify(err || '');\n  if (/anthropic-version/i.test(all)) return 'Box 5 is not sending the anthropic-version header. Open the Anthropic credential in box 5, turn on Add Custom Header, Header Name anthropic-version, Header Value 2023-06-01, Save.';\n  if (/credit balance/i.test(all)) return 'Your Anthropic account has no credit left. Go to console.anthropic.com, Billing, add credit, then press Check again.';\n  if (/spend limit|usage limit|spend cap/i.test(all)) return 'Your Anthropic account hit its spend limit. Go to console.anthropic.com, Limits, raise it, then press Check again.';\n  if (/x-api-key|authentication_error|invalid api key|Authorization failed|\"httpCode\":\"?401/i.test(all)) return 'Anthropic rejected the key. In box 5, open the Anthropic credential, paste a fresh key from console.anthropic.com and wait for the green test.';\n  if (/model/i.test(why) && /not_found|not found|does not exist|invalid|not available|deprecat/i.test(all)) return 'The model name in box 1 is not available on your account. In box 1, set model to claude-sonnet-5-5. (' + why.slice(0, 160) + ')';\n  if (/prompt is too long|too many tokens|request_too_large/i.test(all)) return 'Your Brand Kit plus the draft is too long for one check. Shorten the kit doc, then try again.';\n  if (/overloaded|rate_limit|\\b529\\b|\\b429\\b/i.test(all)) return 'Claude is busy right now. Wait a minute and press Check again.';\n  return 'Claude did not answer. The reason from Anthropic: ' + (why ? why.slice(0, 220) : 'none given') + '. In n8n, open Executions and click box 5 to see the full error.';\n}\n\nif (res.error || !Array.isArray(res.content)) {\n  return fail(explain(res.error || res));\n}\n\nconst text = res.content.filter((c) => c && c.type === 'text').map((c) => c.text).join('\\n').trim();\nif (res.stop_reason === 'max_tokens') {\n  return fail('The answer got cut off because it ran too long. Shorten your Brand Kit doc or the draft, then press Check again.');\n}\nif (res.stop_reason === 'refusal') {\n  return fail('Claude declined to check this draft. Reword it and press Check again.');\n}\n\n// Read the JSON. Structured output makes it plain JSON; the fallbacks cover code fences or a stray sentence.\nfunction readJson(t) {\n  const tries = [t, t.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '')];\n  const a = t.indexOf('{'), b = t.lastIndexOf('}');\n  if (a !== -1 && b > a) tries.push(t.slice(a, b + 1));\n  for (const x of tries) {\n    try { const o = JSON.parse(x); if (o && typeof o === 'object') return o; } catch (e) { /* try the next one */ }\n  }\n  return null;\n}\nconst out = readJson(text);\nif (!out) {\n  return fail('Claude answered, but not in the right format. Press Check again. (Stopped because: ' + (res.stop_reason || 'unknown') + '. It began: ' + text.slice(0, 120).replace(/\\s+/g, ' ') + ')');\n}\n\nconst clamp = (n, max) => Math.max(0, Math.min(max, Math.round(Number(n) || 0)));\nconst p = out.parts || {};\nconst parts = {\n  proof: clamp(p.proof, 30),\n  voice: clamp(p.voice, 25),\n  positioning: clamp(p.positioning, 25),\n  format: clamp(p.format, 20),\n};\nconst score = parts.proof + parts.voice + parts.positioning + parts.format;\nconst hardFails = (Array.isArray(out.hard_fails) ? out.hard_fails : [])\n  .map((h) => String(h).trim())\n  .filter((h) => h && !/^none$/i.test(h));\nconst flags = (Array.isArray(out.flags) ? out.flags : []).slice(0, 6).map((f) => ({\n  quote: String((f && f.quote) || ''),\n  rule: String((f && f.rule) || ''),\n  fix: String((f && f.fix) || ''),\n}));\n\n// Your rule, enforced here so the AI cannot bend it: 80 or more AND zero hard fails.\nconst result = score >= passScore && hardFails.length === 0 ? 'PASS' : 'FAIL';\n\nconst usage = res.usage || {};\nconst cost = (Number(usage.input_tokens) || 0) / 1e6 * priceIn + (Number(usage.output_tokens) || 0) / 1e6 * priceOut;\n\nconst response = {\n  ok: true,\n  score,\n  parts,\n  result,\n  pass_score: passScore,\n  hard_fails: hardFails,\n  pillar: String(out.pillar || 'NONE'),\n  flags,\n  fixed_draft: result === 'PASS' ? build.draft : String(out.fixed_draft || build.draft),\n  one_line: String(out.one_line || ''),\n  cost_usd: Number(cost.toFixed(4)),\n};\n\nreturn finish(response, { cost });\n"
      },
      "id": "f11f26f6-8038-4c4c-af00-d1fe9a73793e",
      "name": "6. Read the answer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1720,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json.response) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "1d2b1569-2fda-40e3-9977-a8de9d67bdac",
      "name": "7. Reply to the page",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1980,
        -120
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "={{ $('1. Your settings').first().json.brand_log_sheet_url }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Brand Log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Date": "={{ $json.log['Date'] }}",
            "Platform": "={{ $json.log['Platform'] }}",
            "Score": "={{ $json.log['Score'] }}",
            "Result": "={{ $json.log['Result'] }}",
            "Hard fails": "={{ $json.log['Hard fails'] }}",
            "Flags": "={{ $json.log['Flags'] }}",
            "Top flag": "={{ $json.log['Top flag'] }}",
            "Pillar": "={{ $json.log['Pillar'] }}",
            "Cost USD": "={{ $json.log['Cost USD'] }}",
            "Draft": "={{ $json.log['Draft'] }}",
            "Fixed draft": "={{ $json.log['Fixed draft'] }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Date",
              "displayName": "Date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Platform",
              "displayName": "Platform",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Score",
              "displayName": "Score",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Result",
              "displayName": "Result",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Hard fails",
              "displayName": "Hard fails",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Flags",
              "displayName": "Flags",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Top flag",
              "displayName": "Top flag",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Pillar",
              "displayName": "Pillar",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Cost USD",
              "displayName": "Cost USD",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Draft",
              "displayName": "Draft",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Fixed draft",
              "displayName": "Fixed draft",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "2b770e6c-ab16-4d00-ae68-2ee71bbb4219",
      "name": "8. Write it in the Brand Log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1980,
        120
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "={{ $json.brand_log_sheet_url }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Brand Log"
        },
        "options": {}
      },
      "id": "c32382fb-075b-4f52-bc1a-d90c9fa5971e",
      "name": "9. Read the Brand Log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        760,
        400
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "// Box 10: turns the last 7 days of your Brand Log into one short report.\n// You do not need to change anything in this box.\nconst weekAgo = $now.minus({ days: 7 });\n\nfunction parseDate(v) {\n  const t = String(v || '').trim();\n  if (!t) return null;\n  const tries = [\n    DateTime.fromFormat(t, 'yyyy-LL-dd HH:mm', { zone: $now.zone }),\n    DateTime.fromISO(t, { zone: $now.zone }),\n    DateTime.fromFormat(t, 'M/d/yyyy H:mm:ss', { zone: $now.zone }),\n    DateTime.fromFormat(t, 'd/M/yyyy H:mm:ss', { zone: $now.zone }),\n  ];\n  return tries.find((d) => d.isValid) || null;\n}\n\nconst rows = $input.all().map((i) => i.json || {}).filter((r) => r.Date);\nconst week = rows.filter((r) => {\n  const d = parseDate(r.Date);\n  return d && d >= weekAgo && d <= $now;\n});\n\nconst checks = week.filter((r) => r.Result === 'PASS' || r.Result === 'FAIL');\nconst passed = checks.filter((r) => r.Result === 'PASS');\nconst failed = checks.filter((r) => r.Result === 'FAIL');\nconst scores = checks.map((r) => Number(r.Score)).filter((n) => !isNaN(n));\nconst avg = scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 0;\nconst cost = week.reduce((a, r) => a + (Number(r['Cost USD']) || 0), 0);\n\n// Count hard fails by their short name (the part before the colon).\nconst hardCount = {};\nfor (const r of checks) {\n  for (const h of String(r['Hard fails'] || '').split(' | ')) {\n    const name = h.split(':')[0].trim();\n    if (name) hardCount[name] = (hardCount[name] || 0) + 1;\n  }\n}\n// Count the most common top flags.\nconst flagCount = {};\nfor (const r of failed) {\n  const f = String(r['Top flag'] || '').trim();\n  if (f) flagCount[f] = (flagCount[f] || 0) + 1;\n}\nconst top = (obj, n) => Object.entries(obj).sort((a, b) => b[1] - a[1]).slice(0, n);\nconst topHard = top(hardCount, 3);\nconst topFlags = top(flagCount, 3);\nconst newRules = top(flagCount, 10).filter(([, c]) => c >= 3);\n\nconst esc = (t) => String(t).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nconst li = (arr) => arr.length ? '<ul>' + arr.map(([k, c]) => `<li>${esc(k)} <b>(${c})</b></li>`).join('') + '</ul>' : '<p>None this week.</p>';\n\nconst rate = checks.length ? Math.round((passed.length / checks.length) * 100) : 0;\nconst subject = checks.length\n  ? `Brand week: ${checks.length} checks, ${passed.length} passed, ${failed.length} blocked`\n  : 'Brand week: no checks this week';\n\nconst html = `\n<div style=\"font-family:Arial,sans-serif;font-size:15px;line-height:1.5;color:#14150f;max-width:560px\">\n  <h2 style=\"margin:0 0 6px\">Your brand week</h2>\n  <p style=\"margin:0 0 16px;color:#555\">${esc(weekAgo.toFormat('d LLL'))} to ${esc($now.toFormat('d LLL yyyy'))}</p>\n  <table style=\"border-collapse:collapse;margin-bottom:16px\">\n    <tr><td style=\"padding:4px 16px 4px 0\">Drafts checked</td><td><b>${checks.length}</b></td></tr>\n    <tr><td style=\"padding:4px 16px 4px 0\">Passed</td><td><b>${passed.length}</b> (${rate}%)</td></tr>\n    <tr><td style=\"padding:4px 16px 4px 0\">Blocked before posting</td><td><b>${failed.length}</b></td></tr>\n    <tr><td style=\"padding:4px 16px 4px 0\">Average score</td><td><b>${avg}</b> / 100</td></tr>\n    <tr><td style=\"padding:4px 16px 4px 0\">AI cost this week</td><td><b>$${cost.toFixed(2)}</b></td></tr>\n  </table>\n  <h3 style=\"margin:16px 0 4px\">Hard fails caught</h3>${li(topHard)}\n  <h3 style=\"margin:16px 0 4px\">What blocked you most</h3>${li(topFlags)}\n  <h3 style=\"margin:16px 0 4px\">Rules to add to your Brand Kit</h3>\n  ${newRules.length ? '<p>These came up 3 times or more. Add each one to part 6 or part 11 of your kit, so the checker catches it sooner:</p>' + li(newRules) : '<p>Nothing repeated 3 times. Your kit is holding.</p>'}\n  <p style=\"margin-top:20px;color:#555\">Sent by your Brand Checker. The full log is in your Brand Log sheet.</p>\n</div>`;\n\nreturn [{ json: { subject, html, checks: checks.length, passed: passed.length, failed: failed.length, avg, cost: Number(cost.toFixed(2)) } }];\n"
      },
      "id": "61b5a928-4432-452a-bbe2-9f3ed582bb55",
      "name": "10. Write the weekly report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1000,
        400
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $('1. Your settings').first().json.report_email }}",
        "subject": "={{ $json.subject }}",
        "emailType": "html",
        "message": "={{ $json.html }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "75f0b72f-3a70-4a43-900e-50d36e4466cd",
      "name": "11. Email me the report",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1240,
        400
      ]
    },
    {
      "parameters": {
        "content": "## START HERE (about 10 minutes)\n1. Box 1: paste the link to your Brand Kit Google Doc and your Brand Log Google Sheet. Put your email in report_email.\n2. Box 3: pick your Google Docs account. Boxes 8 and 9: your Google Sheets account. Box 11: your Gmail.\n3. Box 5: Credential type Anthropic, Create new, paste your API key. Turn on Add Custom Header: name anthropic-version, value 2023-06-01. Save. It must say Connection tested successfully.\n4. Save. Switch the workflow to Active (top right).\n5. Open box 'Check a draft', copy the Production URL, paste it into the Brand Checker page once.\n\nEvery check: about 1.5 cents. Every Sunday at 6 PM: one report email.",
        "height": 400,
        "width": 560,
        "color": 5
      },
      "id": "13712d46-63b6-431a-9755-a135e920268b",
      "name": "Start here",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -460
      ]
    }
  ],
  "connections": {
    "Check a draft": {
      "main": [
        [
          {
            "node": "1. Your settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Sunday at 6 PM": {
      "main": [
        [
          {
            "node": "1. Your settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Your settings": {
      "main": [
        [
          {
            "node": "Is it a check?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is it a check?": {
      "main": [
        [
          {
            "node": "2. Checker rules",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "9. Read the Brand Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Checker rules": {
      "main": [
        [
          {
            "node": "3. Get my Brand Kit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Get my Brand Kit": {
      "main": [
        [
          {
            "node": "4. Build the question",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Build the question": {
      "main": [
        [
          {
            "node": "5. Ask Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Ask Claude": {
      "main": [
        [
          {
            "node": "6. Read the answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6. Read the answer": {
      "main": [
        [
          {
            "node": "7. Reply to the page",
            "type": "main",
            "index": 0
          },
          {
            "node": "8. Write it in the Brand Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9. Read the Brand Log": {
      "main": [
        [
          {
            "node": "10. Write the weekly report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "10. Write the weekly report": {
      "main": [
        [
          {
            "node": "11. Email me the report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "meta": {
    "templateCredsSetupCompleted": false
  }
}