{
  "name": "JAH · Content + Marketing Ops · Content Multiplier",
  "nodes": [
    {
      "parameters": {
        "content": "## Content Multiplier\n\nPaste one blog post in. Get 4 pieces of content out.\n\nYou get:\n1. A tweet thread (5 tweets)\n2. A LinkedIn post\n3. An Instagram caption\n4. A newsletter block\n\n### 5 minute setup\n1. Open the Config (EDIT ME) node.\n2. Type your business name and voice.\n3. Keep testMode set to true.\n4. Run the test in the green note.\n5. Later: add your OpenAI key and set testMode to false.",
        "height": 560,
        "width": 420
      },
      "id": "sticky-header",
      "name": "Sticky Header",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -480,
        200
      ]
    },
    {
      "parameters": {
        "content": "## Placeholders to fill\n\n1. YOUR_BUSINESS_NAME: in Config (EDIT ME).\n2. brandVoice: in Config (EDIT ME).\n3. OPENAI_API_KEY: add it as an n8n environment variable. The live AI node reads it.\n4. YOUR_NOTION_DATABASE_ID_OR_SHEET_ID: in Config (EDIT ME).\n5. Save Pack node: swap the NoOp for your Notion or Google Sheets node.\n\nYou need zero keys for test mode.",
        "height": 400,
        "width": 420,
        "color": 5
      },
      "id": "sticky-creds",
      "name": "Sticky Credentials",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -480,
        800
      ]
    },
    {
      "parameters": {
        "content": "## Run it right now (zero keys)\n\n1. Activate the workflow.\n2. Send a POST to the webhook path jah-test-jah-content-multiplier.\n3. Body: { \"title\": \"My post\", \"long_form_text\": \"paste 300 or more characters here\" }\n4. You get 4 channel items back in the response.\n\nTest mode uses mock AI output. Zero cost. Zero keys.",
        "height": 250,
        "width": 860,
        "color": 4
      },
      "id": "sticky-test",
      "name": "Sticky Test Mode",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        40
      ]
    },
    {
      "parameters": {
        "content": "## 1) Intake and guard\n\nThe webhook takes a POST with your blog post.\nThe guard checks the text is at least 40 characters.\nBad input gets a clear error back. No wasted AI calls.",
        "height": 300,
        "width": 860
      },
      "id": "sticky-intake",
      "name": "Sticky Intake",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        340
      ]
    },
    {
      "parameters": {
        "content": "## 2) Multiply and split\n\nTest mode ON: the mock node returns a sample 4 format pack.\nTest mode OFF: one AI call writes all 4 formats as JSON.\nThe splitter turns the pack into 4 items, one per channel.\nEach item is ready to save as a row or page.",
        "height": 620,
        "width": 1180
      },
      "id": "sticky-multiply",
      "name": "Sticky Multiply",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        840,
        160
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "jah-test-jah-content-multiplier",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-in",
      "name": "Blog Post In",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        460
      ],
      "webhookId": "3f7a2c10-64d1-4b7e-9a3e-c0de00000010"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cfg-1",
              "name": "testMode",
              "value": true,
              "type": "boolean"
            },
            {
              "id": "cfg-2",
              "name": "businessName",
              "value": "YOUR_BUSINESS_NAME",
              "type": "string"
            },
            {
              "id": "cfg-3",
              "name": "brandVoice",
              "value": "Plain, friendly, confident. Short sentences. No hype.",
              "type": "string"
            },
            {
              "id": "cfg-4",
              "name": "saveTarget",
              "value": "YOUR_NOTION_DATABASE_ID_OR_SHEET_ID",
              "type": "string"
            },
            {
              "id": "cfg-5",
              "name": "openAiModel",
              "value": "gpt-4o-mini",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "config-set",
      "name": "Config (EDIT ME)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        460
      ]
    },
    {
      "parameters": {
        "jsCode": "// Pull the blog post out of the webhook body and carry config fields forward.\nconst cfg = $input.first().json;\nconst data = cfg.body || {};\nconst text = (data.long_form_text || data.text || data.content || '').toString().trim();\nconst title = (data.title || 'Untitled post').toString().trim();\nconst sourceUrl = (data.source_url || '').toString().trim();\nreturn [{ json: {\n  testMode: cfg.testMode === true || cfg.testMode === 'true',\n  businessName: (cfg.businessName || 'your business').toString(),\n  brandVoice: (cfg.brandVoice || 'plain and friendly').toString(),\n  saveTarget: (cfg.saveTarget || '').toString(),\n  openAiModel: (cfg.openAiModel || 'gpt-4o-mini').toString(),\n  title: title,\n  source_url: sourceUrl,\n  long_form_text: text.substring(0, 6000),\n  long_form_ok: text.length >= 40\n} }];"
      },
      "id": "prep-input",
      "name": "Prep Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        460
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": ""
          },
          "conditions": [
            {
              "id": "v1",
              "leftValue": "={{ $json.long_form_ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-valid",
      "name": "Valid Input?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        660,
        460
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: 'error', message: 'Send long_form_text with at least 40 characters.' }) }}",
        "options": {}
      },
      "id": "respond-missing",
      "name": "Respond Missing Text",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        880,
        700
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": ""
          },
          "conditions": [
            {
              "id": "t1",
              "leftValue": "={{ $json.testMode }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-test",
      "name": "Test Mode?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        880,
        380
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "mock-1",
              "name": "ai_raw",
              "value": "{\"tweet_thread\":[\"Your water heater warns you before it dies. Most people miss all 5 signs. Here they are:\",\"1. Rusty water from the hot tap only. That is the tank rusting from the inside out.\",\"2. Rumbling or popping sounds. Sediment is baking onto the bottom of the tank.\",\"3. Water pooling around the base. Small leaks turn into floods fast.\",\"4. Hot water runs out in minutes. 5. The unit is past 10 years old. Catch these early and you can save 2,000 dollars in water damage.\"],\"linkedin_post\":\"A customer called me last month about a small puddle under a 12 year old water heater. The tank failed 4 days later. The cleanup bill was 2,400 dollars. The warning signs were there for weeks. Rusty hot water. Popping sounds. Slow recovery. A 15 minute check once a year catches all of this. If your unit is past 8 years old, book an inspection this month. It costs less than 1 percent of a flood.\",\"instagram_caption\":\"Your water heater is talking to you. Are you listening?\\n\\n5 warning signs most homeowners miss, saved in today's post. Number 3 is the one that floods basements.\\n\\n#homemaintenance #waterheater #plumbingtips #homeownertips\",\"newsletter_block\":\"IS YOUR WATER HEATER ABOUT TO QUIT?\\n\\nMost water heaters give you weeks of warning before they fail. Rusty hot water means the tank is corroding inside. Popping sounds mean sediment buildup. A damp base means a leak has already started. If your unit is past 8 years old, check it this weekend. A 15 minute look can save you a 2,000 dollar cleanup.\"}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "mock-pack",
      "name": "Mock Content Pack (test)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1100,
        260
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.OPENAI_API_KEY || '' }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: $json.openAiModel || 'gpt-4o-mini', max_tokens: 1200, messages: [{ role: 'user', content: `You write marketing content for ${$json.businessName}. Voice: ${$json.brandVoice}. Turn the blog post below into 4 formats. Respond with ONLY raw JSON, no markdown fences, shaped exactly: {\"tweet_thread\": [\"<tweet 1>\", \"<tweet 2>\", \"<tweet 3>\", \"<tweet 4>\", \"<tweet 5>\"], \"linkedin_post\": \"<200-300 word post>\", \"instagram_caption\": \"<hook line plus caption plus 3-5 hashtags>\", \"newsletter_block\": \"<120-180 word newsletter section with a header line>\"}. Keep each tweet under 280 characters. Make the first tweet a strong hook.\\n\\nTitle: ${$json.title}\\nSource URL: ${$json.source_url}\\nBlog post:\\n${$json.long_form_text}` }] }) }}",
        "options": {}
      },
      "id": "ai-multiply",
      "name": "AI Multiply Content (live)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        500
      ]
    },
    {
      "parameters": {
        "jsCode": "// Parse the 4 format pack from mock or live AI output.\nconst item = $input.first().json;\nconst src = $('Prep Input').first().json;\nconst raw = item.choices?.[0]?.message?.content || item.ai_raw || '{}';\nlet pack = {};\ntry { pack = JSON.parse(raw.replace(/```json|```/g, '').trim()); } catch (e) { pack = { parse_error: e.message }; }\nconst thread = Array.isArray(pack.tweet_thread) ? pack.tweet_thread.map(t => (t || '').toString().substring(0, 280)) : [];\nconst channels = [\n  { channel: 'twitter_thread', content: thread.join('\\n\\n'), parts: thread.length },\n  { channel: 'linkedin', content: (pack.linkedin_post || '').toString(), parts: 1 },\n  { channel: 'instagram', content: (pack.instagram_caption || '').toString(), parts: 1 },\n  { channel: 'newsletter', content: (pack.newsletter_block || '').toString(), parts: 1 }\n];\nreturn [{ json: {\n  title: src.title,\n  source_url: src.source_url,\n  testMode: src.testMode,\n  saveTarget: src.saveTarget,\n  parse_error: pack.parse_error || '',\n  channels: channels\n} }];"
      },
      "id": "parse-pack",
      "name": "Parse Content Pack",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        380
      ]
    },
    {
      "parameters": {
        "fieldToSplitOut": "channels",
        "include": "allOtherFields",
        "options": {}
      },
      "id": "split-channels",
      "name": "Split Channels",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1540,
        380
      ]
    },
    {
      "parameters": {
        "jsCode": "// Shape one clean row per channel, ready for Notion or Sheets.\nconst rows = $input.all().map(i => {\n  const j = i.json;\n  const ch = j.channels || j;\n  return { json: {\n    channel: (ch.channel || '').toString(),\n    content: (ch.content || '').toString(),\n    parts: Number(ch.parts || 1),\n    title: (j.title || '').toString(),\n    source_url: (j.source_url || '').toString(),\n    status: 'draft',\n    save_target: (j.saveTarget || '').toString(),\n    created_at: new Date().toISOString()\n  } };\n});\nreturn rows;"
      },
      "id": "format-rows",
      "name": "Format Save Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        380
      ]
    },
    {
      "parameters": {},
      "id": "save-pack",
      "name": "Save Pack (add your Notion or Sheets node)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1980,
        380
      ]
    },
    {
      "parameters": {
        "respondWith": "allIncomingItems",
        "options": {}
      },
      "id": "respond-pack",
      "name": "Respond With Pack",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2200,
        380
      ]
    }
  ],
  "connections": {
    "Blog Post In": {
      "main": [
        [
          {
            "node": "Config (EDIT ME)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config (EDIT ME)": {
      "main": [
        [
          {
            "node": "Prep Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep Input": {
      "main": [
        [
          {
            "node": "Valid Input?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valid Input?": {
      "main": [
        [
          {
            "node": "Test Mode?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Missing Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test Mode?": {
      "main": [
        [
          {
            "node": "Mock Content Pack (test)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "AI Multiply Content (live)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mock Content Pack (test)": {
      "main": [
        [
          {
            "node": "Parse Content Pack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Multiply Content (live)": {
      "main": [
        [
          {
            "node": "Parse Content Pack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Content Pack": {
      "main": [
        [
          {
            "node": "Split Channels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Channels": {
      "main": [
        [
          {
            "node": "Format Save Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Save Rows": {
      "main": [
        [
          {
            "node": "Save Pack (add your Notion or Sheets node)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Pack (add your Notion or Sheets node)": {
      "main": [
        [
          {
            "node": "Respond With Pack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
