{
  "name": "AI Twitter Thread Writer",
  "nodes": [
    {
      "id": "7b2c5d33-aaaa-4111-8111-100000000001",
      "name": "On form submission",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "formTitle": "Blog URL to Viral Twitter Thread",
        "formDescription": "Paste any article URL. The AI reads it and writes a hook-first X/Twitter thread ready to post.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Article URL",
              "fieldType": "text",
              "requiredField": true,
              "placeholder": "https://example.com/blog/post"
            },
            {
              "fieldLabel": "Thread Tone",
              "fieldType": "dropdown",
              "requiredField": true,
              "fieldOptions": {
                "values": [
                  {
                    "option": "Developer / technical"
                  },
                  {
                    "option": "Startup / founder"
                  },
                  {
                    "option": "Hot take / contrarian"
                  },
                  {
                    "option": "Educational explainer"
                  }
                ]
              }
            },
            {
              "fieldLabel": "Target Tweet Count",
              "fieldType": "dropdown",
              "requiredField": true,
              "fieldOptions": {
                "values": [
                  {
                    "option": "5"
                  },
                  {
                    "option": "7"
                  },
                  {
                    "option": "9"
                  }
                ]
              }
            }
          ]
        },
        "options": {}
      },
      "webhookId": "twitter-thread-form"
    },
    {
      "id": "7b2c5d33-aaaa-4222-8222-200000000002",
      "name": "Fetch Article HTML",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        232,
        0
      ],
      "parameters": {
        "url": "={{ $json['Article URL'] }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text",
              "neverError": true
            }
          },
          "timeout": 15000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; n8n-thread-writer/1.0)"
            }
          ]
        }
      }
    },
    {
      "id": "7b2c5d33-aaaa-4333-8333-300000000003",
      "name": "Extract Clean Text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        472,
        0
      ],
      "parameters": {
        "jsCode": "const html = $input.first().json.data || $input.first().json.body || '';\nconst articleUrl = $('On form submission').item.json['Article URL'];\nconst tone = $('On form submission').item.json['Thread Tone'];\nconst tweetCount = $('On form submission').item.json['Target Tweet Count'];\n\n// Extract <title>\nconst titleMatch = html.match(/<title[^>]*>([^<]+)<\\/title>/i);\nconst title = titleMatch ? titleMatch[1].trim() : 'Untitled article';\n\n// Extract meta description\nconst descMatch = html.match(/<meta[^>]+name=[\"']description[\"'][^>]+content=[\"']([^\"']+)[\"']/i);\nconst description = descMatch ? descMatch[1] : '';\n\n// Strip tags, scripts, styles; keep text only\nconst cleaned = html\n  .replace(/<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi, ' ')\n  .replace(/<style\\b[^>]*>[\\s\\S]*?<\\/style>/gi, ' ')\n  .replace(/<nav\\b[^>]*>[\\s\\S]*?<\\/nav>/gi, ' ')\n  .replace(/<footer\\b[^>]*>[\\s\\S]*?<\\/footer>/gi, ' ')\n  .replace(/<header\\b[^>]*>[\\s\\S]*?<\\/header>/gi, ' ')\n  .replace(/<[^>]+>/g, ' ')\n  .replace(/&nbsp;/g, ' ')\n  .replace(/&amp;/g, '&')\n  .replace(/&lt;/g, '<')\n  .replace(/&gt;/g, '>')\n  .replace(/&quot;/g, '\"')\n  .replace(/\\s+/g, ' ')\n  .trim();\n\n// Cap at ~6000 chars to keep the prompt affordable\nconst body = cleaned.slice(0, 6000);\n\nreturn [{ json: { title, description, body, articleUrl, tone, tweetCount } }];"
      }
    },
    {
      "id": "7b2c5d33-aaaa-4444-8444-400000000004",
      "name": "Write Thread",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.5,
      "position": [
        712,
        0
      ],
      "parameters": {
        "promptType": "define",
        "text": "=You are a professional X/Twitter writer. Read the ARTICLE below and write a ready-to-post thread.\n\nARTICLE TITLE: {{ $json.title }}\nARTICLE URL: {{ $json.articleUrl }}\nMETA DESCRIPTION: {{ $json.description }}\n\nARTICLE BODY:\n\"\"\"\n{{ $json.body }}\n\"\"\"\n\nTHREAD REQUIREMENTS:\n- Tone: {{ $json.tone }}\n- Total tweets: exactly {{ $json.tweetCount }}\n- Each tweet \u2264 270 chars (leave room for hand editing)\n- Tweet 1 = a scroll-stopping hook that promises a specific payoff. No \"In this thread I will cover...\" openings.\n- Middle tweets = one idea each. Use concrete numbers, proper nouns, and specifics from the article \u2014 never vague generalities.\n- Last tweet = CTA. Include the article URL and ask for a follow + bookmark.\n- No hashtag spam. Max 1 well-chosen hashtag in the whole thread.\n- No emojis unless the tone is Hot take / contrarian (then max 1 emoji in the hook).\n\nOUTPUT FORMAT (strict):\nReturn a JSON object, nothing else:\n{\n  \"hook\": \"<tweet 1 text>\",\n  \"tweets\": [\"<tweet 2>\", \"<tweet 3>\", ...],\n  \"cta\": \"<final tweet text>\",\n  \"preview_card_headline\": \"<6-8 word headline if you were to redesign the article's og:image>\"\n}",
        "messages": {
          "messageValues": []
        },
        "batching": {},
        "hasOutputParser": true
      }
    },
    {
      "id": "7b2c5d33-aaaa-4555-8555-500000000005",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        648,
        224
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini"
        },
        "options": {}
      }
    },
    {
      "id": "7b2c5d33-aaaa-4666-8666-600000000006",
      "name": "Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.2,
      "position": [
        800,
        224
      ],
      "parameters": {
        "jsonSchemaExample": "{\n  \"hook\": \"string\",\n  \"tweets\": [\"string\"],\n  \"cta\": \"string\",\n  \"preview_card_headline\": \"string\"\n}"
      }
    }
  ],
  "connections": {
    "On form submission": {
      "main": [
        [
          {
            "node": "Fetch Article HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Article HTML": {
      "main": [
        [
          {
            "node": "Extract Clean Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Clean Text": {
      "main": [
        [
          {
            "node": "Write Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Write Thread",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Write Thread",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null
}