This workflow corresponds to n8n.io template #15327 — we link there as the canonical source.
This workflow follows the Facebookgraphapi → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "Szelm1dSKH9lUyGB",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Monthly AI SEO Content Engine: 34 Assets to WordPress, LinkedIn & Socials",
"tags": [],
"nodes": [
{
"id": "d14b741c-9077-42e3-b739-fd6f676d2967",
"name": "\ud83d\udccb Template \u00b7 Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
1296,
3424
],
"parameters": {
"width": 976,
"height": 2654,
"content": "## Monthly AI SEO Content Engine: 34 Assets to WordPress, LinkedIn & Socials\n\n### Who it's for\nMarketing agencies, SEO consultants, and brand owners looking to automate a high-volume, multi-platform content strategy while strictly maintaining brand voice.\n\n### What it does\nThis workflow is an end-to-end content engine. It generates a comprehensive brand strategy, then automatically writes, formats, and publishes 34 monthly assets (SEO blogs, LinkedIn posts, social media updates, newsletters, and long-form guides) across multiple platforms.\n\n### How it works\n- **Strategy:** Collects business context via an n8n form and uses Google Gemini to save a master brand strategy document to Google Drive.\n- **Generation:** Analyzes your Google Analytics data and Google Trends, writes 34 targeted content pieces, and compiles them into a formatted Word (.docx) package.\n- **Publishing:** Extracts the generated text, creates custom AI images for each piece using Gemini Flash Image, and staggers the publishing schedule across WordPress, LinkedIn, Facebook, and Instagram.\n\n### How to set up\n1. Run the Strategy pipeline via the Form Trigger to generate your brand's Markdown file.\n2. In the Generation pipeline, update the `Config: Pipeline B` node with your Google Analytics Property ID, notification email, the Drive URL of your Markdown file, and your preferred Google Trends geographic and language codes.\n3. In the Publishing pipeline, update `Config: Pipeline C` with your Facebook Page ID, Instagram ID, WordPress domain, S3 Bucket and Folder name and a notification email address.\n4. Test manually using the trigger nodes before enabling the Schedule triggers.\n\n### Requirements\n- **n8n version 1.60+** (required for the node types used in this template).\n- **AWS S3 bucket with public-read access** (required to host images for Instagram publishing).\n- Google Gemini API credentials (budget for ~100k-150k output tokens per monthly run).\n- Google Workspace (Analytics, Drive, Gmail).\n- WordPress site with Basic Auth enabled.\n- LinkedIn account, Facebook Page, and Instagram Business account.\n\n### How to customize the workflow\n- Modify the Schedule trigger cron expressions to publish weekly instead of monthly.\n- Adjust the prompt templates inside the Code nodes to change the content formats or output volume."
},
"typeVersion": 1
},
{
"id": "8a0ee76c-ec80-4570-abed-b6dcc1a1dc3e",
"name": "Company Data Form",
"type": "n8n-nodes-base.formTrigger",
"position": [
2448,
3664
],
"parameters": {
"options": {},
"formTitle": "Master Business Context Generator",
"formFields": {
"values": [
{
"fieldLabel": "Company Name",
"requiredField": true
},
{
"fieldLabel": "Industry",
"requiredField": true
},
{
"fieldLabel": "Primary Services (Comma Separated)",
"requiredField": true
},
{
"fieldType": "textarea",
"fieldLabel": "Mission",
"requiredField": true
},
{
"fieldType": "textarea",
"fieldLabel": "Vision",
"requiredField": true
},
{
"fieldLabel": "Tagline",
"requiredField": true
},
{
"fieldType": "textarea",
"fieldLabel": "Core Services (Format: Name: Description, one per line)"
},
{
"fieldType": "textarea",
"fieldLabel": "Target Audience (Format: Persona: Role, one per line)"
},
{
"fieldType": "textarea",
"fieldLabel": "Client Problems (One per line)"
},
{
"fieldType": "textarea",
"fieldLabel": "Why Choose Us (One per line)"
}
]
},
"formDescription": "Fill in your company details to auto-generate a 16-section AI strategy document."
},
"typeVersion": 2.2
},
{
"id": "134ad88a-44d2-4ffc-abc6-53c7e8a62c5a",
"name": "Build Prompt",
"type": "n8n-nodes-base.code",
"position": [
2704,
3664
],
"parameters": {
"jsCode": "const schemaDescription = `\\nYou must return a single valid JSON object matching this exact structure.\\nNo preamble, no markdown fences, no explanation \u2014 only the raw JSON.\\n\\nRequired top-level keys and their types:\\n\\ncompany_overview: object with keys:\\n name (string), industry (string), primary_services (array of strings),\\n mission (string), vision (string), tagline (string)\\n\\ncore_services: array of objects, each with:\\n name (string), description (string), key_deliverables (array of strings),\\n key_topics (array of strings), client_outcomes (array of strings)\\n\\ntarget_audience: array of objects, each with:\\n persona_name (string), role (string), company_type (string),\\n pain_point (string), goal (string), buys_service (string)\\n\\nprimary_client_types: array of objects, each with:\\n type (string), description (string), pain_points (array of strings)\\n\\ncustomer_pain_points: array of strings\\n\\nvalue_propositions: object with:\\n reasons_clients_choose_us (array of strings), differentiators (array of strings)\\n\\ncompetitor_differentiation: object with:\\n vs_large_consulting_firms (array of strings),\\n vs_generic_agencies (array of strings),\\n vs_diy_tools (array of strings),\\n key_positioning_statement (string)\\n\\nbrand_voice: object with:\\n tone (array of strings), messaging_themes (array of strings),\\n use_language_like (array of strings), avoid_language_like (array of strings)\\n\\nprohibited_phrases: array of strings\\n\\nkey_content_topics: array of objects, each with:\\n service_name (string), topics (array of strings)\\n\\nseo_keywords: object with:\\n primary (array of strings), secondary (array of strings), long_tail (array of strings)\\n\\ncontent_strategy: object with:\\n monthly_targets: object with keys blogs (integer), linkedin_posts (integer),\\n educational_social_posts (integer), email_newsletters (integer), long_form_guides (integer)\\n content_pillars (array of strings)\\n\\nmarketing_channels: object with:\\n primary (array of strings), secondary (array of strings)\\n\\nindustry_trends: array of strings\\n\\ntechnical_integrations: array of strings\\n\\ninternal_workflow: array of objects, each with:\\n role (string), responsibility (string)\\n\\nFill every field thoroughly. No empty strings. No placeholders.\\n`;\nconst input = $input.first().json;\nconst textToArray = (text) => {\n if (!text) return [];\n return text.split('\\n').map(l => l.trim()).filter(l => l.length > 0);\n};\nconst parseKeyValue = (text, keyName, valName) => {\n return textToArray(text).map(line => {\n const parts = line.split(':');\n return { [keyName]: parts[0]?.trim() || '', [valName]: parts[1]?.trim() || ''};\n }).filter(item => item[keyName]);\n};\nconst coreServices = parseKeyValue(input['Core Services (Format: Name: Description, one per line)'], 'name', 'description');\nconst targetAudience = parseKeyValue(input['Target Audience (Format: Persona: Role, one per line)'], 'persona_name', 'role');\nconst clientProblems = textToArray(input['Client Problems (One per line)']);\nconst whyChooseUs = textToArray(input['Why Choose Us (One per line)']);\n\nconst userMessage = `Generate a complete Master Business Context document based on the company details below.\\n\\nCompany Name: ${input['Company Name']}\\nIndustry: ${input['Industry']}\\nPrimary Service: ${input['Primary Services']}\\nMission: ${input['Mission']}\\nVision: ${input['Vision']}\\nTagline: ${input['Tagline']}\\n\\nCore Services:\\n${JSON.stringify(coreServices)}\\n\\nTarget Audience Names/Roles:\\n${JSON.stringify(targetAudience)}\\n\\nCommon Problems Clients Face:\\n${JSON.stringify(clientProblems)}\\n\\nWhy Clients Choose Us:\\n${JSON.stringify(whyChooseUs)}`;\n\nconst prompt = {\n systemInstruction: `You are a professional business content writer specializing in AI consulting companies.\\nYour job is to generate complete, structured Master Business Context documents.\\nAlways write in a confident, expert, and approachable tone.\\nNever use generic clich\u00e9s like \"game-changer\", \"leverage\", \"seamless\", \"cutting-edge\",\\n\"revolutionize\", \"unlock potential\", \"synergy\", \"best-in-class\", \"holistic approach\".\\nMake every section specific and actionable \u2014 no filler content.\\n\\n${schemaDescription}`,\n userMessage,\n \"generationConfig\": {\n \"temperature\": 0.4,\n \"maxOutputTokens\": 8192,\n \"responseMimeType\": \"application/json\"\n }\n};\nreturn { json: prompt };"
},
"typeVersion": 2
},
{
"id": "11dfc5c1-3a29-4982-927d-71a74b5a62d0",
"name": "Parse Response",
"type": "n8n-nodes-base.code",
"position": [
3312,
3664
],
"parameters": {
"jsCode": "const items = $input.first().json\nconst text = items.content?.parts?.[0]?.text ?? '';\nconst cleanJson = text.replace(/```json|```/g, \"\").trim();\nconst response = JSON.parse(cleanJson)\nreturn {response}"
},
"typeVersion": 2
},
{
"id": "35c2a2f6-d1dc-4d14-9bc0-adfd82a45963",
"name": "Format to Markdown",
"type": "n8n-nodes-base.code",
"position": [
3600,
3664
],
"parameters": {
"jsCode": "const inputJson = [$input.first().json];\n\nfunction generateMarkdownFromData(jsonData) {\n const data = jsonData[0].response;\n let md = \"# AI Agency \u2013 Master Business Context\\n\";\n md += \"Version: 2.0\\n\";\n md += `Last Updated: ${new Date().getFullYear()}\\n\\n---\\n\\n`;\n const renderList = (items) => items.map(item => `- ${item}`).join('\\n');\n\n if (data.company_overview) {\n const co = data.company_overview;\n md += `# Company Overview\\n\\nCompany Name: ${co.name}\\nIndustry: ${co.industry}\\nPrimary Services: ${co.primary_services.join(', ')}\\n\\nMission:\\n${co.mission}\\n\\nVision:\\n${co.vision}\\n\\nTagline:\\n${co.tagline}\\n\\n---\\n\\n`;\n }\n if (data.core_services) {\n md += `# Core Services\\n\\n`;\n data.core_services.forEach((svc, i) => {\n md += `## ${i + 1}. ${svc.name}\\n\\nDescription:\\n${svc.description}\\n\\n`;\n if (svc.key_deliverables) md += `Key Deliverables:\\n${renderList(svc.key_deliverables)}\\n\\n`;\n if (svc.key_topics) md += `Key Topics:\\n${renderList(svc.key_topics)}\\n\\n`;\n if (svc.client_outcomes) md += `Client Outcomes:\\n${renderList(svc.client_outcomes)}\\n\\n`;\n md += `---\\n\\n`;\n });\n }\n if (data.target_audience) {\n md += `# Target Audience\\n\\n## Client Personas\\n\\n`;\n data.target_audience.forEach((aud, i) => {\n md += `### Persona ${i + 1} \u2013 ${aud.persona_name}\\nRole: ${aud.role}\\nCompany: ${aud.company_type}\\nPain: ${aud.pain_point}\\nGoal: ${aud.goal}\\nBuys: ${aud.buys_service}\\n\\n`;\n });\n md += `---\\n\\n`;\n }\n if (data.primary_client_types) {\n md += `# Primary Client Types\\n\\n`;\n data.primary_client_types.forEach((c, i) => {\n md += `## ${i + 1}. ${c.type}\\n${c.description}\\n\\nPain Points:\\n${renderList(c.pain_points)}\\n\\n`;\n });\n md += `---\\n\\n`;\n }\n if (data.customer_pain_points) md += `# Customer Pain Points\\n\\n${renderList(data.customer_pain_points)}\\n\\n---\\n\\n`;\n if (data.value_propositions) {\n md += `# Value Propositions\\n\\nWhy Clients Choose Us:\\n${renderList(data.value_propositions.reasons_clients_choose_us)}\\n\\nDifferentiators:\\n${renderList(data.value_propositions.differentiators)}\\n\\n---\\n\\n`;\n }\n if (data.competitor_differentiation) {\n const cd = data.competitor_differentiation;\n md += `# Competitor Differentiation\\n\\nvs. Large Consulting Firms:\\n${renderList(cd.vs_large_consulting_firms)}\\n\\nvs. Generic IT Agencies:\\n${renderList(cd.vs_generic_agencies)}\\n\\nvs. DIY/Pure Automation Tools:\\n${renderList(cd.vs_diy_tools)}\\n\\nKey Positioning Statement:\\n${cd.key_positioning_statement}\\n\\n---\\n\\n`;\n }\n if (data.brand_voice) {\n const bv = data.brand_voice;\n md += `# Brand Voice & Messaging\\n\\nTone:\\n${renderList(bv.tone)}\\n\\nMessaging Themes:\\n${renderList(bv.messaging_themes)}\\n\\nUse language like this:\\n${renderList(bv.use_language_like)}\\n\\nAvoid language like this:\\n${renderList(bv.avoid_language_like)}\\n\\n---\\n\\n`;\n }\n if (data.prohibited_phrases) md += `# Prohibited Phrases & Clich\u00e9s\\n\\n${renderList(data.prohibited_phrases)}\\n\\n---\\n\\n`;\n if (data.key_content_topics) {\n md += `# Key Content Topics\\n\\n`;\n data.key_content_topics.forEach(tg => { md += `## ${tg.service_name} Topics\\n${renderList(tg.topics)}\\n\\n`; });\n md += `---\\n\\n`;\n }\n if (data.seo_keywords) {\n const seo = data.seo_keywords;\n md += `# SEO Keywords\\n\\nPrimary:\\n${renderList(seo.primary)}\\n\\nSecondary:\\n${renderList(seo.secondary)}\\n\\nLong-Tail:\\n${renderList(seo.long_tail)}\\n\\n---\\n\\n`;\n }\n if (data.content_strategy) {\n const cs = data.content_strategy;\n md += `# Content Strategy\\n\\n## Monthly Content Targets\\nBlogs: ${cs.monthly_targets.blogs}\\nLinkedIn Posts: ${cs.monthly_targets.linkedin_posts}\\nEducational Social Posts: ${cs.monthly_targets.educational_social_posts}\\nEmail Newsletters: ${cs.monthly_targets.email_newsletters}\\nLong-Form Guides: ${cs.monthly_targets.long_form_guides}\\n\\n## Content Pillars\\n`;\n cs.content_pillars.forEach((p, i) => { md += `${i + 1}. ${p}\\n`; });\n md += `\\n---\\n\\n`;\n }\n if (data.marketing_channels) {\n md += `# Marketing Channels\\n\\nPrimary:\\n${renderList(data.marketing_channels.primary)}\\n\\nSecondary:\\n${renderList(data.marketing_channels.secondary)}\\n\\n---\\n\\n`;\n }\n if (data.industry_trends) md += `# Industry Trends\\n\\n${renderList(data.industry_trends)}\\n\\n---\\n\\n`;\n if (data.technical_integrations) md += `# Technical Integrations\\n\\n${renderList(data.technical_integrations)}\\n\\n---\\n\\n`;\n if (data.internal_workflow) {\n md += `# Internal Workflow\\n\\n## Team Roles\\n\\n`;\n data.internal_workflow.forEach(m => { md += `${m.role}:\\n${m.responsibility}\\n\\n`; });\n md += `---\\n\\n`;\n }\n md += `# End of File\\n`;\n return md;\n}\n\nconst finalMarkdown = generateMarkdownFromData(inputJson);\nreturn {data: finalMarkdown}"
},
"typeVersion": 2
},
{
"id": "48658195-a7a4-41e8-ba85-a33abc1afa0f",
"name": "Export MD File",
"type": "n8n-nodes-base.convertToFile",
"position": [
3904,
3664
],
"parameters": {
"options": {
"fileName": "={{ $('Parse Response').item.json.response.company_overview.name }} AI Context.md"
},
"operation": "toText",
"sourceProperty": "data"
},
"typeVersion": 1.1
},
{
"id": "3e226295-f40e-42f9-af7a-d8137517d884",
"name": "Save to Drive",
"type": "n8n-nodes-base.googleDrive",
"position": [
4224,
3664
],
"parameters": {
"name": "={{ $binary.data.fileName }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive",
"cachedResultUrl": "https://drive.google.com/drive/my-drive",
"cachedResultName": "My Drive"
},
"options": {},
"folderId": {
"__rl": true,
"mode": "list",
"value": "root",
"cachedResultUrl": "https://drive.google.com/drive",
"cachedResultName": "/ (Root folder)"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 3
},
{
"id": "4e36a718-23ca-4a07-b076-54b9a6c9de46",
"name": "Fetch GA Data",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
3280,
4224
],
"parameters": {
"url": "=https://analyticsdata.googleapis.com/v1beta/properties/{{ $json.property_id }}:runReport",
"method": "POST",
"options": {},
"jsonBody": "{\n \"dateRanges\": [{\"startDate\": \"30daysAgo\", \"endDate\": \"today\"}],\n \"dimensions\": [{\"name\": \"pagePath\"}, {\"name\": \"pageTitle\"}],\n \"metrics\": [{\"name\": \"sessions\"}, {\"name\": \"bounceRate\"}, {\"name\": \"averageSessionDuration\"}],\n \"orderBys\": [{\"metric\": {\"metricName\": \"sessions\"}, \"desc\": true}]\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleAnalyticsOAuth2"
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 4.2,
"alwaysOutputData": true
},
{
"id": "4585b237-ff17-4d8d-877e-fdee2435c911",
"name": "Fetch Trending Topics",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueErrorOutput",
"position": [
3280,
4448
],
"parameters": {
"url": "=https://trends.google.com/trending/rss?geo={{ $('Config: Pipeline B').item.json.trends_geo }}&hl={{ $('Config: Pipeline B').item.json.trends_lang }}",
"options": {
"response": {
"response": {
"responseFormat": "text",
"outputPropertyName": "trends_data"
}
}
}
},
"typeVersion": 4.2
},
{
"id": "40ed2e5d-0802-4be8-8413-02f0da9fdede",
"name": "Merge Data Sources",
"type": "n8n-nodes-base.merge",
"position": [
4000,
4224
],
"parameters": {
"numberInputs": 3
},
"typeVersion": 3,
"alwaysOutputData": true
},
{
"id": "f6edf885-aa38-4601-9a95-6a13388ee6a6",
"name": "Compress Brand Context",
"type": "n8n-nodes-base.code",
"position": [
4352,
4240
],
"parameters": {
"jsCode": "const items = $input.all();\nconst mdItem = items.find(i => i.json.master_data !== undefined);\nconst gaItem = items.find(i => i.json.rows !== undefined || i.json.error !== undefined);\nconst trendsItem = items.find(i => i.json.trends_data !== undefined);\nconst md = mdItem?.json?.master_data ?? '';\nconst ga = gaItem?.json ?? {};\nconst trends = trendsItem?.json?.trends_data ?? '';\nif (!md) throw new Error('Fetch Master MD returned empty \u2014 check the URL and HTTP node.');\nfunction extractCompanyName(md) {\n const overview = extractSection(md, 'Company Overview');\n if (!overview) return '';\n\n const match = overview.match(/Company Name:\\s*(.*)/i);\n return match ? match[1].trim() : '';\n}\nfunction extractSection(text, heading) {\n const chunks = text.split(/\\n(?=# )/);\n for (const chunk of chunks) {\n const firstLine = chunk.split('\\n')[0].trim();\n const title = firstLine.replace(/^#+\\s*/, '');\n if (title.toLowerCase() === heading.toLowerCase()) {\n return chunk.trim();\n }\n }\n return '';\n}\nconst compressed = [\n extractSection(md, 'Core Services'),\n extractSection(md, 'Target Audience'),\n extractSection(md, 'SEO Keywords'),\n extractSection(md, 'Prohibited Phrases & Clich\u00e9s'),\n extractSection(md, 'Brand Voice & Messaging')\n].filter(Boolean).join('\\n\\n---\\n\\n');\nconst gaRows = ga.rows ?? [];\nconst top5Sessions = gaRows\n .slice(0, 5)\n .map(r => `${r.dimensionValues?.[1]?.value ?? 'Unknown'}: ${r.metricValues?.[0]?.value ?? 0} sessions`)\n .join('\\n');\nconst top5Bounce = [...gaRows]\n .sort((a, b) => parseFloat(b.metricValues?.[1]?.value ?? 0) - parseFloat(a.metricValues?.[1]?.value ?? 0))\n .slice(0, 5)\n .map(r => `${r.dimensionValues?.[1]?.value ?? 'Unknown'}: ${(parseFloat(r.metricValues?.[1]?.value ?? 0) * 100).toFixed(0)}% bounce`)\n .join('\\n');\nconst gaSummary = gaRows.length > 0\n ? `Top pages by sessions:\\n${top5Sessions}\\n\\nHigh-bounce pages (content gaps):\\n${top5Bounce}`\n : 'Analytics data unavailable \u2014 proceed without gap analysis';\nconst AI_KEYWORDS = [\n 'ai', 'artificial intelligence', 'machine learning', 'ml', 'llm',\n 'automation', 'automat', 'chatgpt', 'generative', 'genai', 'gpt',\n 'agentic', 'agent', 'copilot', 'workflow', 'saas', 'digital',\n 'enterprise software', 'productivity', 'cloud', 'data analytics',\n 'prompt', 'openai', 'gemini', 'claude', 'chatbot', 'robotic',\n 'rpa', 'nlp', 'deep learning', 'neural', 'digital transformation',\n 'business automation', 'ai tools', 'ai strategy', 'technology'\n];\nfunction parseTrendItems(xml) {\n const items = [];\n const itemRe = /<item>([\\s\\S]*?)<\\/item>/g;\n let m;\n while ((m = itemRe.exec(xml)) !== null) {\n const itemXml = m[1];\n const titleM = itemXml.match(/<title>(?:<!\\[CDATA\\[)?(.*?)(?:\\]\\]>)?<\\/title>/);\n const trafficM = itemXml.match(/<ht:approx_traffic>(.*?)<\\/ht:approx_traffic>/);\n const newsTitles = [...itemXml.matchAll(/<ht:news_item_title>(?:<!\\[CDATA\\[)?(.*?)(?:\\]\\]>)?<\\/ht:news_item_title>/g)]\n .map(n => n[1].trim());\n if (titleM) {\n items.push({\n title: titleM[1].trim(),\n traffic: trafficM ? parseInt(trafficM[1].replace(/[^0-9]/g, '')) || 0 : 0,\n newsTitles\n });\n }\n }\n return items;\n}\nfunction isRelevant(item) {\n const searchText = (item.title + ' ' + item.newsTitles.join(' ')).toLowerCase();\n return AI_KEYWORDS.some(kw => searchText.includes(kw));\n}\nconst allTrends = parseTrendItems(trends);\nconst aiTrends = allTrends.filter(isRelevant).sort((a, b) => b.traffic - a.traffic);\nconst topAiTrends = aiTrends.slice(0, 8);\nlet trendsSummary;\nif (topAiTrends.length > 0) {\n const lines = topAiTrends.map(t => {\n const traffic = t.traffic >= 1000 ? `${(t.traffic/1000).toFixed(0)}k+` : `${t.traffic}+`;\n const context = t.newsTitles[0] ? ` \u2014 \"${t.newsTitles[0].slice(0, 80)}\"` : '';\n return ` \u2022 ${t.title} (${traffic} searches)${context}`;\n }).join('\\n');\n trendsSummary = `AI & tech topics trending in India right now:\\n${lines}`;\n} else {\n trendsSummary = `No AI/tech topics in today's Google Trends India feed (trending topics are unrelated: ${allTrends.slice(0,5).map(t => t.title).join(', ')}). Use evergreen AI business topics relevant to Indian enterprises instead.`;\n}\nconst BATCH_SECTIONS = ['Core Services', 'Target Audience', 'Customer Pain Points', 'Value Propositions', 'Key Content Topics'];\nconst GUIDES_SECTIONS = [...BATCH_SECTIONS, 'Competitor Differentiation'];\nconst batchWritingContext = BATCH_SECTIONS.map(s => extractSection(md, s)).filter(Boolean).join('\\n\\n');\nconst guidesWritingContext = GUIDES_SECTIONS.map(s => extractSection(md, s)).filter(Boolean).join('\\n\\n');\nconst toneBlock = ['Brand Voice & Messaging', 'Prohibited Phrases & Clich\u00e9s']\n .map(s => extractSection(md, s)).filter(Boolean).join('\\n\\n');\n\nconst companyName = extractCompanyName(md);\nreturn [{\n json: {\n companyName: companyName,\n compressed_brand: compressed,\n full_brand: md,\n batch_writing_context: batchWritingContext,\n guides_writing_context: guidesWritingContext,\n tone_block: toneBlock,\n ga_summary: gaSummary,\n trends_summary: trendsSummary,\n trends_ai_count: topAiTrends.length,\n trends_total_count: allTrends.length,\n run_date: new Date().toISOString().split('T')[0]\n }\n}];"
},
"typeVersion": 2
},
{
"id": "1e6f1d6b-5979-4e36-8d31-2ca1dcf0644a",
"name": "Build Content Plan Prompt",
"type": "n8n-nodes-base.code",
"position": [
4624,
4240
],
"parameters": {
"jsCode": "const compressedBrand = $input.first().json.compressed_brand;\nconst companyName = $input.first().json.companyName;\nconst gaSummary = $input.first().json.ga_summary;\nconst trendsSummary = $input.first().json.trends_summary;\n\nconst prompt = {\n \"systemInstruction\": \"You are a senior AI content strategist. Return ONLY valid JSON with no preamble, no markdown fences, and no explanation.\",\n \"userMessage\": `You are planning monthly AI marketing content for ${companyName} \u2014 an IT consulting agency with four service pillars: AI Strategy Consulting, AI Training & Fluency, Agentic AI Systems, Business Automation.\\n\\nBrand context (compressed):\\n${compressedBrand}\\n\\nWebsite analytics summary:\\n${gaSummary}\\n\\nTrending topics this month:\\n${trendsSummary}\\n\\nInstructions:\\n- Use analytics high-bounce pages to identify content gaps \u2014 prioritize filling those gaps.\\n- Use trending topics to rank ideas by rising search interest.\\n- Distribute content evenly across all 4 pillars. No pillar should have more than 2 blog topics.\\n- Blog primary keywords must be search-viable and non-branded.\\n- LinkedIn hooks must be specific \u2014 no openers starting with \"AI is transforming...\" or \"In today's world...\".\\n- Long-tail keywords must include intent signals: \"how to\", \"for enterprise\", \"ROI of\", or comparison terms.\\n- Never use these phrases: game-changer, leverage, unlock potential, cutting-edge, seamless, robust, disruptive.\\n\\nReturn ONLY this JSON schema \u2014 no other text:\\n{\\n \"strategy_overview\": \"2-3 sentence summary citing specific gaps or trends identified\",\\n \"blog_topics\": [\\n {\"title\": \"\", \"primary_keyword\": \"\", \"content_pillar\": \"AI Strategy|AI Training|Agentic AI|Automation\", \"target_persona\": \"CTO|Operations Leader|Innovation Builder|L&D Manager\"}\\n ],\\n \"linkedin_posts\": [\\n {\"topic\": \"\", \"hook\": \"\", \"content_pillar\": \"AI Strategy|AI Training|Agentic AI|Automation\"}\\n ],\\n \"social_posts\": [\\n {\"topic\": \"\", \"format\": \"tip|stat|question|checklist\"}\\n ],\\n \"email_topics\": [\\n {\"subject\": \"\", \"content_pillar\": \"AI Strategy|AI Training|Agentic AI|Automation\"}\\n ],\\n \"guides\": [\\n {\"title\": \"\", \"content_pillar\": \"AI Strategy|AI Training|Agentic AI|Automation\"}\\n ],\\n \"keyword_clusters\": {\\n \"ai_strategy\": [],\\n \"agentic_ai\": [],\\n \"automation\": [],\\n \"ai_training\": []\\n },\\n \"long_tail_keywords\": []\\n}\\n\\nCounts required: 6 blog_topics, 12 linkedin_posts, 8 social_posts, 4 email_topics, 4 guides, 5-8 terms per keyword cluster, 10 long_tail_keywords.`,\n \"generationConfig\": {\n \"temperature\": 0.4,\n \"maxOutputTokens\": 8192,\n \"responseMimeType\": \"application/json\",\n thinkingConfig: {\n thinkingBudget: 0\n }\n }\n};\n\nreturn { json: prompt };"
},
"typeVersion": 2
},
{
"id": "8a55e57d-5144-4c38-814e-a7e52d8896bb",
"name": "Validate Content Plan",
"type": "n8n-nodes-base.code",
"position": [
2384,
4768
],
"parameters": {
"jsCode": "const response = $input.all()[0].json;\nif (response?.error) {\n throw new Error('Gemini API error: ' + JSON.stringify(response.error));\n}\nconst rawText = response?.content?.parts?.[0]?.text ?? '';\nconst finishReason = response?.finish_reason ?? 'UNKNOWN';\nif (!rawText) {\n throw new Error('Gemini returned empty content. Full response: ' + JSON.stringify(response).slice(0, 500));\n}\nlet text = rawText.replace(/^```json\\s*/m, '').replace(/\\s*```$/m, '').trim();\nif ((text.startsWith('\"') && text.endsWith('\"')) ||\n (text.startsWith(\"'\") && text.endsWith(\"'\"))) {\n text = text.slice(1, -1)\n .replace(/\\\\\"/g, '\"')\n .replace(/\\\\n/g, '\\n')\n .replace(/\\\\\\\\/g, '\\\\');\n}\nlet plan = null;\nlet parseError = null;\ntry {\n plan = JSON.parse(text);\n if (typeof plan === 'string') {\n plan = JSON.parse(plan);\n }\n} catch (e) {\n parseError = e.message;\n plan = null;\n}\nif (!plan || typeof plan !== 'object' || Array.isArray(plan)) {\n function extractArray(src, key) {\n const re = new RegExp('\"' + key + '\"\\\\s*:\\\\s*(\\\\[[\\\\s\\\\S]*?\\\\])(?=\\\\s*[,}])', '');\n const m = src.match(re);\n if (m) {\n try { return JSON.parse(m[1]); } catch (e) { return null; }\n }\n return null;\n }\n function extractString(src, key) {\n const re = new RegExp('\"' + key + '\"\\\\s*:\\\\s*\"((?:[^\"\\\\\\\\]|\\\\\\\\.)*)\"');\n const m = src.match(re);\n return m ? m[1] : null;\n }\n plan = {\n strategy_overview: extractString(text, 'strategy_overview') || 'Strategy overview unavailable (response truncated)',\n blog_topics: extractArray(text, 'blog_topics') || [],\n linkedin_posts: extractArray(text, 'linkedin_posts') || [],\n social_posts: extractArray(text, 'social_posts') || [],\n email_topics: extractArray(text, 'email_topics') || [],\n guides: extractArray(text, 'guides') || [],\n keyword_clusters: extractArray(text, 'keyword_clusters') || {},\n long_tail_keywords: extractArray(text, 'long_tail_keywords') || []\n };\n}\nconst required = [\n ['blog_topics', 6],\n ['linkedin_posts', 12],\n ['social_posts', 8],\n ['email_topics', 4],\n ['guides', 4]\n];\nconst failures = required\n .filter(([key, count]) => !Array.isArray(plan[key]) || plan[key].length < count)\n .map(([key, count]) => key + ': expected ' + count + ', got ' + (plan[key]?.length ?? 0));\nif (failures.length > 0) {\n const hint = finishReason === 'MAX_TOKENS'\n ? ' Gemini hit MAX_TOKENS \u2014 increase maxOutputTokens or reduce prompt size.'\n : '';\n throw new Error(\n 'Content plan validation failed:' + hint + '\\n' +\n failures.join('\\n') +\n '\\n\\nfinishReason: ' + finishReason +\n '\\nRaw output (first 600 chars):\\n' + rawText.slice(0, 600)\n );\n}\nreturn [{ json: { content_plan: plan, finish_reason: finishReason } }];\n"
},
"typeVersion": 2
},
{
"id": "d82125b3-5f3a-4df6-8ddc-648d7a7c13e1",
"name": "Build Batch Prompt",
"type": "n8n-nodes-base.code",
"position": [
2672,
4688
],
"parameters": {
"jsCode": "const companyName = $('Compress Brand Context').first().json.companyName;\nconst plan = $('Validate Content Plan').first().json.content_plan;\nconst batchContext = $('Compress Brand Context').first().json.batch_writing_context;\nconst toneBlock = $('Compress Brand Context').first().json.tone_block;\n\nconst blogTopics = JSON.stringify(plan.blog_topics, null, 2);\nconst linkedinPosts = JSON.stringify(plan.linkedin_posts, null, 2);\nconst socialPosts = JSON.stringify(plan.social_posts, null, 2);\nconst emailTopics = JSON.stringify(plan.email_topics, null, 2);\n\nconst user_prompt = `Brand context:\n${batchContext}\n\nYou will write four content types in sequence. Separate each type with EXACTLY this delimiter on its own line: ===SECTION: [TYPE]===\n\n---\n===SECTION: BLOGS===\n\nWrite all 6 SEO blog articles for these topics:\n${blogTopics}\n\nFor each article include:\n- SEO title (primary keyword used naturally, not forced)\n- Meta description (150-160 characters)\n- Introduction (150 words \u2014 frame the business problem clearly)\n- 4-5 H2 sections with H3 subheadings where appropriate\n- Real business scenarios (no invented company names)\n- Practical implementation steps a reader can act on\n- 5-point key takeaways\n- Soft CTA for [Your Brand] consulting (2-3 sentences, non-pushy)\n- Length: 1200-1500 words each\n- Separate articles with: --- ARTICLE BREAK ---\n- Label each: ARTICLE 1: [Title], ARTICLE 2: [Title], etc.\n\n===SECTION: LINKEDIN===\n\nWrite all 12 LinkedIn thought leadership posts for these topics:\n${linkedinPosts}\n\nFor each post include:\n- Opening hook (specific question, stat, or bold statement \u2014 NOT \"AI is transforming...\" or \"In today's world...\")\n- 150-250 words of practical insight\n- 1 clear, actionable takeaway\n- 5-8 relevant hashtags\n- First-person voice as a senior AI consultant\n- Mix formats across the 12: some narrative, some list-style, some question-driven\n- Separate with: --- POST BREAK ---\n- Label each: POST 1, POST 2, etc.\n\n===SECTION: SOCIAL===\n\nWrite all 8 educational social media posts for these topics:\n${socialPosts}\n\nFor each post include:\n- Engaging hook\n- Core content 80-120 words matching the plan format (tip/stat/question/checklist)\n- 1 practical insight the reader can use immediately\n- 4-6 hashtags\n- Separate with: --- POST BREAK ---\n- Label each: SOCIAL 1 [FORMAT], SOCIAL 2 [FORMAT], etc.\n\n===SECTION: EMAILS===\n\nWrite all 4 email newsletters for these topics:\n${emailTopics}\n\nFor each newsletter include:\n- Subject line (under 50 characters, no clickbait)\n- Preview text (under 90 characters)\n- Opening hook (2-3 sentences that earn the read)\n- Main content (300-400 words: insight, trend, or how-to)\n- Secondary block: brief tip or resource placeholder\n- Soft CTA (conversation invite, guide link, or free assessment offer)\n- Sign-off from [Your Brand] team\n- Separate with: --- EMAIL BREAK ---\n- Label each: EMAIL 1, EMAIL 2, etc.`\n\nconst systemInstruction = `You are a professional AI content writer for ${companyName}.\n\n## CRITICAL: OUTPUT STRUCTURE\nYou MUST return a single valid JSON object. No markdown, no backticks, no explanation outside the JSON.\nThe JSON must follow this exact structure:\n\n{\n \"blog_articles\": [\n {\n \"article_title\": \"ARTICLE 1: [Title]\",\n \"seo_title\": \"string\",\n \"meta_description\": \"string (150-160 chars)\",\n \"introduction\": \"string\",\n \"sections\": [\n {\n \"h2\": \"string\",\n \"h3\": \"string\",\n \"content\": \"string\"\n }\n ],\n \"key_takeaways\": [\"string\", \"string\", \"string\", \"string\", \"string\"],\n \"call_to_action\": \"string\",\n \"word_count\": 0\n }\n ],\n \"linkedin_posts\": [\n {\n \"post_number\": \"POST 1\",\n \"topic\": \"string\",\n \"hook\": \"string\",\n \"body\": \"string\",\n \"takeaway\": \"string\",\n \"hashtags\": [\"string\"]\n }\n ],\n \"social_posts\": [\n {\n \"post_number\": \"SOCIAL 1\",\n \"format\": \"tip | stat | question | checklist\",\n \"topic\": \"string\",\n \"hook\": \"string\",\n \"content\": \"string\",\n \"insight\": \"string\",\n \"hashtags\": [\"string\"]\n }\n ],\n \"email_newsletters\": [\n {\n \"email_number\": \"EMAIL 1\",\n \"subject_line\": \"string (under 50 chars)\",\n \"preview_text\": \"string (under 90 chars)\",\n \"opening_hook\": \"string\",\n \"main_content\": \"string\",\n \"secondary_block\": \"string\",\n \"call_to_action\": \"string\",\n \"sign_off\": \"string\"\n }\n ]\n}\n\nRules:\n- blog_articles must have exactly 6 items\n- linkedin_posts must have exactly 12 items\n- social_posts must have exactly 8 items\n- email_newsletters must have exactly 4 items\n- All strings must be properly escaped for JSON\n- Do NOT include ===SECTION=== delimiters inside the JSON\n- Do NOT wrap the JSON in markdown code blocks`;\n\nconst prompt = {\n \"systemInstruction\": systemInstruction,\n \"userMessage\": user_prompt,\n generationConfig: {\n temperature: 0.75,\n maxOutputTokens: 120000,\n thinkingConfig: {\n thinkingBudget: 0\n }\n }\n};\n\nreturn { json: prompt };"
},
"typeVersion": 2
},
{
"id": "9fefd545-80da-4cb5-8641-ee47bb24911b",
"name": "Build Guides Prompt",
"type": "n8n-nodes-base.code",
"position": [
2672,
4912
],
"parameters": {
"jsCode": "const companyName = $('Compress Brand Context').first().json.companyName;\nconst plan = $('Validate Content Plan').first().json.content_plan;\nconst guidesContext = $('Compress Brand Context').first().json.guides_writing_context;\nconst toneBlock = $('Compress Brand Context').first().json.tone_block;\n\nconst guideTopics = JSON.stringify(plan.guides, null, 2);\nconst user_prompt = `Brand context:\n${guidesContext}\n\n---\n\nWrite all 4 AI guides for these topics:\n${guideTopics}\n\nEach guide must include:\n- Title and subtitle\n- Executive summary (100 words)\n- 5-7 structured H2 sections\n- A framework or decision model where applicable\n- 2-3 realistic business scenarios (no invented company names)\n- Common mistakes section\n- Recommended next steps (numbered list)\n- How [Your Brand] can help (2-3 sentences, brief and non-pushy)\n\nTarget length: 1800-2000 words each.\nSeparate each guide with: --- GUIDE BREAK ---\nLabel each: GUIDE 1: [Title], GUIDE 2: [Title], etc.`\nconst prompt = {\n \"systemInstruction\": `You are a senior AI strategist and writer for ${companyName}.\nWrite authoritative, practical guides for business leaders making investment decisions.\nTone: direct, data-informed, results-focused.\n\nTONE & VOICE:\n${toneBlock}`,\n \"userMessage\": user_prompt,\n generationConfig: {\n temperature: 0.6,\n maxOutputTokens: 8192,\n thinkingConfig: {\n thinkingBudget: 0\n }\n }\n};\n\nreturn { json: prompt };"
},
"typeVersion": 2
},
{
"id": "bcb8143c-5f0d-4088-9031-674d86cf3e6e",
"name": "Parse Content Sections",
"type": "n8n-nodes-base.code",
"position": [
3392,
4688
],
"parameters": {
"jsCode": "const response = $input.first().json;\nif (response?.error) {\n throw new Error('Gemini batch call API error: ' + JSON.stringify(response.error));\n}\n\n// Gemini node in n8n may return parsed JSON directly or as a text string\nlet parsed;\n\nif (response.blog_articles || response.linkedin_posts) {\n // n8n already parsed the JSON (responseMimeType: application/json)\n parsed = response;\n} else {\n // Fallback: extract raw text and parse manually\n const raw = response?.[0]?.content?.parts?.[0]?.text\n ?? response?.content?.parts?.[0]?.text\n ?? '';\n\n if (!raw) {\n throw new Error('Gemini: Write Batch Content returned empty. Check API key, quota, and node output.');\n }\n\n try {\n const clean = raw.replace(/```json|```/g, '').trim();\n parsed = JSON.parse(clean);\n } catch (e) {\n throw new Error('Failed to parse Gemini JSON response: ' + e.message + '\\n\\nRaw preview: ' + raw.substring(0, 300));\n }\n}\n\n// Validate expected sections\nconst warnings = [];\nif (!Array.isArray(parsed.blog_articles) || parsed.blog_articles.length === 0) warnings.push('blog_articles missing or empty');\nif (!Array.isArray(parsed.linkedin_posts) || parsed.linkedin_posts.length === 0) warnings.push('linkedin_posts missing or empty');\nif (!Array.isArray(parsed.social_posts) || parsed.social_posts.length === 0) warnings.push('social_posts missing or empty');\nif (!Array.isArray(parsed.email_newsletters) || parsed.email_newsletters.length === 0) warnings.push('email_newsletters missing or empty');\n\n// Validate counts\nif (parsed.blog_articles?.length !== 6) warnings.push(`blog_articles count is ${parsed.blog_articles?.length}, expected 6`);\nif (parsed.linkedin_posts?.length !== 12) warnings.push(`linkedin_posts count is ${parsed.linkedin_posts?.length}, expected 12`);\nif (parsed.social_posts?.length !== 8) warnings.push(`social_posts count is ${parsed.social_posts?.length}, expected 8`);\nif (parsed.email_newsletters?.length !== 4) warnings.push(`email_newsletters count is ${parsed.email_newsletters?.length}, expected 4`);\n\nif (warnings.length > 0) {\n console.warn('Parse Batch Sections warnings: ' + warnings.join(', '));\n}\n\nreturn [{\n json: {\n blog_articles: parsed.blog_articles ?? [],\n linkedin_posts: parsed.linkedin_posts ?? [],\n social_posts: parsed.social_posts ?? [],\n email_newsletters: parsed.email_newsletters ?? [],\n content_plan: $('Validate Content Plan').first().json.content_plan,\n parse_warnings: warnings\n }\n}];"
},
"typeVersion": 2
},
{
"id": "ff3bde36-74a1-4533-b699-edb25e226210",
"name": "Extract Guides",
"type": "n8n-nodes-base.code",
"position": [
3392,
4912
],
"parameters": {
"jsCode": "const response = $input.first().json;\nif (response?.error) {\n throw new Error('Gemini guides call API error: ' + JSON.stringify(response.error));\n}\nconst guidesText = response?.content?.parts?.[0]?.text ?? '[MISSING: Guides \u2014 Gemini: Write Guides returned empty. Check API key and token limits.]';\nreturn [{ json: { ai_guides: guidesText } }];"
},
"typeVersion": 2
},
{
"id": "ef563ded-5e36-47a6-9c0b-ef2653bc3e02",
"name": "Merge Content Output",
"type": "n8n-nodes-base.merge",
"position": [
3776,
4816
],
"parameters": {},
"typeVersion": 3,
"alwaysOutputData": true
},
{
"id": "3493f35d-f97e-498a-b7ab-0c747300017b",
"name": "Build DOCX",
"type": "n8n-nodes-base.code",
"position": [
4176,
4816
],
"parameters": {
"jsCode": "let inp = {};\ntry { inp = $input.first().json || {}; } catch(e) { inp = {}; }\n\nconst docTitle = inp.doc_title || 'AI Content Package';\nconst runDate = inp.run_date || new Date().toLocaleDateString('en-IN',{month:'long',year:'numeric'});\nconst plan = inp.content_plan || {};\nconst assembled = inp.assembled_content || '';\n\nconst NAVY='1E3A5F', BLUE='2E75B6', LBLUE='EBF3FB', ALT='F5F7FA';\nconst BODY='444444', MUTED='777777', WHITE='FFFFFF', CHARCOAL='2B2B2B';\n\nconst esc = s => String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');\n\nfunction rPr(o){\n o=o||{};\n return `<w:rPr>${o.bold?'<w:b/>':''}${o.italic?'<w:i/>':''}<w:color w:val=\"${o.color||BODY}\"/><w:sz w:val=\"${o.size||22}\"/><w:szCs w:val=\"${o.size||22}\"/><w:rFonts w:ascii=\"${o.font||'Calibri'}\" w:hAnsi=\"${o.font||'Calibri'}\" w:cs=\"${o.font||'Calibri'}\"/></w:rPr>`;\n}\n\nfunction run(text,o){ return `<w:r>${rPr(o)}<w:t xml:space=\"preserve\">${esc(text)}</w:t></w:r>`; }\n\nfunction para(runs,o){\n o=o||{};\n const sb=o.spaceBefore!==undefined?o.spaceBefore:60, sa=o.spaceAfter!==undefined?o.spaceAfter:60;\n const jc=o.align?`<w:jc w:val=\"${o.align}\"/>`:'';\n const sp=`<w:spacing w:before=\"${sb}\" w:after=\"${sa}\"/>`;\n const bd=o.borderBottom?`<w:pBdr><w:bottom w:val=\"single\" w:sz=\"8\" w:space=\"4\" w:color=\"${o.borderBottom}\"/></w:pBdr>`:'';\n return `<w:p><w:pPr>${bd}${sp}${jc}</w:pPr>${Array.isArray(runs)?runs.join(''):runs}</w:p>`;\n}\n\nconst h1 = t => para(run(t,{bold:true,color:NAVY,size:32}),{spaceBefore:240,spaceAfter:80,borderBottom:BLUE});\nconst h2 = t => para(run(t,{bold:true,color:BLUE,size:26}),{spaceBefore:160,spaceAfter:60});\nconst h3 = t => para(run(t,{bold:true,color:CHARCOAL,size:24}),{spaceBefore:140,spaceAfter:40});\nconst h4 = t => para(run(t,{bold:true,color:BODY,size:22}),{spaceBefore:100,spaceAfter:30});\nconst bodyP = t => para(run(t,{color:BODY,size:22}),{spaceBefore:40,spaceAfter:40});\nconst mutedP = t => para(run(t,{color:MUTED,size:20,italic:true}),{spaceBefore:40,spaceAfter:40});\nconst bulletP = t => `<w:p><w:pPr><w:numPr><w:ilvl w:val=\"0\"/><w:numId w:val=\"1\"/></w:numPr><w:spacing w:before=\"40\" w:after=\"40\"/></w:pPr>${run(t,{color:BODY,size:22})}</w:p>`;\nconst numP = (t,nid) => `<w:p><w:pPr><w:numPr><w:ilvl w:val=\"0\"/><w:numId w:val=\"${nid||2}\"/></w:numPr><w:spacing w:before=\"40\" w:after=\"40\"/></w:pPr>${run(t,{color:BODY,size:22})}</w:p>`;\nconst spacerP = () => `<w:p><w:pPr><w:spacing w:before=\"40\" w:after=\"40\"/></w:pPr></w:p>`;\nconst pgBreak = () => `<w:p><w:r><w:br w:type=\"page\"/></w:r></w:p>`;\nconst dividerP = () => `<w:p><w:pPr><w:pBdr><w:bottom w:val=\"single\" w:sz=\"2\" w:space=\"2\" w:color=\"${LBLUE}\"/></w:pBdr><w:spacing w:before=\"80\" w:after=\"80\"/></w:pPr></w:p>`;\nconst labelValueP = (label,value) => `<w:p><w:pPr><w:spacing w:before=\"40\" w:after=\"40\"/></w:pPr>${run(label+': ',{bold:true,color:NAVY,size:22})}${run(value,{color:BODY,size:22})}</w:p>`;\nconst takeawayP = value => `<w:p><w:pPr><w:spacing w:before=\"80\" w:after=\"80\"/><w:shd w:val=\"clear\" w:color=\"auto\" w:fill=\"${LBLUE}\"/></w:pPr>${run('Actionable Takeaway: ',{bold:true,color:NAVY,size:20})}${run(value,{color:CHARCOAL,size:20})}</w:p>`;\n\nfunction cell(text,width,fill,bold,color){\n fill=fill||WHITE; bold=bold||false; color=color||CHARCOAL;\n const shd=`<w:shd w:val=\"clear\" w:color=\"auto\" w:fill=\"${fill}\"/>`;\n const bdr=`<w:tcBorders><w:top w:val=\"single\" w:sz=\"2\" w:color=\"D0D9E3\"/><w:bottom w:val=\"single\" w:sz=\"2\" w:color=\"D0D9E3\"/><w:left w:val=\"single\" w:sz=\"2\" w:color=\"D0D9E3\"/><w:right w:val=\"single\" w:sz=\"2\" w:color=\"D0D9E3\"/></w:tcBorders>`;\n const mar=`<w:tcMar><w:top w:w=\"80\" w:type=\"dxa\"/><w:bottom w:w=\"80\" w:type=\"dxa\"/><w:left w:w=\"140\" w:type=\"dxa\"/><w:right w:w=\"140\" w:type=\"dxa\"/></w:tcMar>`;\n const p=para(run(text,{bold,color,size:20}),{spaceBefore:0,spaceAfter:0});\n return `<w:tc><w:tcPr><w:tcW w:w=\"${width}\" w:type=\"dxa\"/>${bdr}${shd}${mar}</w:tcPr>${p}</w:tc>`;\n}\nconst hCell = (t,w) => cell(t,w,NAVY,true,WHITE);\nconst trow = cells => `<w:tr>${cells.join('')}</w:tr>`;\nconst tbl = (W,cols,rows) => {\n const grid=cols.map(w=>`<w:gridCol w:w=\"${w}\"/>`).join('');\n return `<w:tbl><w:tblPr><w:tblW w:w=\"${W}\" w:type=\"dxa\"/><w:tblLayout w:type=\"fixed\"/><w:tblCellMar><w:top w:w=\"0\" w:type=\"dxa\"/><w:bottom w:w=\"0\" w:type=\"dxa\"/></w:tblCellMar></w:tblPr><w:tblGrid>${grid}</w:tblGrid>${rows.join('')}</w:tbl>`;\n};\n\nfunction cover(){\n const nob=`<w:tcBorders><w:top w:val=\"none\"/><w:bottom w:val=\"none\"/><w:left w:val=\"none\"/><w:right w:val=\"none\"/></w:tcBorders>`;\n const mar=`<w:tcMar><w:top w:w=\"300\" w:type=\"dxa\"/><w:bottom w:w=\"300\" w:type=\"dxa\"/><w:left w:w=\"400\" w:type=\"dxa\"/><w:right w:w=\"400\" w:type=\"dxa\"/></w:tcMar>`;\n const c1=`<w:tc><w:tcPr><w:tcW w:w=\"9360\" w:type=\"dxa\"/>${nob}<w:shd w:val=\"clear\" w:color=\"auto\" w:fill=\"${NAVY}\"/>${mar}</w:tcPr>`\n +para(run('[YOUR COMPANY NAME]',{bold:true,color:WHITE,size:52}),{spaceBefore:0,spaceAfter:80})\n +para(run('AI Content Pipeline \\u2014 Monthly Package',{color:'BDD7EE',size:28}),{spaceBefore:0,spaceAfter:0})\n +`</w:tc>`;\n const c2=`<w:tc><w:tcPr><w:tcW w:w=\"9360\" w:type=\"dxa\"/>${nob}<w:shd w:val=\"clear\" w:color=\"auto\" w:fill=\"${LBLUE}\"/>${mar}</w:tcPr>`\n +para(run('Generated: '+runDate+' | Pipeline: Optimized',{color:BLUE,size:22}),{spaceBefore:0,spaceAfter:60})\n +para(run('Powered by n8n Automation',{color:MUTED,size:20}),{spaceBefore:0,spaceAfter:0})\n +`</w:tc>`;\n return tbl(9360,[9360],[trow([c1])])+tbl(9360,[9360],[trow([c2])])+spacerP();\n}\n\nfunction summaryTbl(){\n const d=[\n ['SEO Blog Articles','6'],\n ['LinkedIn Thought Leadership Posts','12'],\n ['Educational Social Media Posts','8'],\n ['Email Newsletter Campaigns','4'],\n ['AI Strategy & Automation Guides','4'],\n ['TOTAL','34']\n ];\n return tbl(9360,[7200,2160],[\n trow([hCell('Content Type',7200),hCell('Count',2160)]),\n ...d.map(([t,c],i)=>trow([\n cell(t,7200,i%2?ALT:WHITE,t==='TOTAL',t==='TOTAL'?NAVY:CHARCOAL),\n cell(c,2160,i%2?ALT:WHITE,true,BLUE)\n ]))\n ]);\n}\n\nfunction tocTbl(){\n const rows=[\n ['1','Monthly Content Strategy Overview'],\n ['2','SEO Blog Articles (6)'],\n ['3','LinkedIn Thought Leadership Posts (12)'],\n ['4','Educational Social Media Posts (8)'],\n ['5','Email Newsletter Campaigns (4)'],\n ['6','AI Strategy & Automation Guides (4)'],\n ['7','Keyword Clusters & Long-Tail Keywords']\n ];\n return tbl(9360,[720,8640],[\n trow([hCell('#',720),hCell('Section',8640)]),\n ...rows.map(([n,s],i)=>trow([\n cell(n,720,i%2?ALT:WHITE,true,BLUE),\n cell(s,8640,i%2?ALT:WHITE)\n ]))\n ]);\n}\n\nfunction blogSummaryTbl(){\n const items=plan.blog_topics||[];\n if(!items.length) return '';\n return tbl(9360,[5040,2160,2160],[\n trow([hCell('Blog Title',5040),hCell('Keyword',2160),hCell('Pillar',2160)]),\n ...items.map((b,i)=>trow([\n cell(b.title||'',5040,i%2?ALT:WHITE),\n cell(b.primary_keyword||'',2160,i%2?ALT:WHITE,false,BLUE),\n cell(b.content_pillar||'',2160,i%2?ALT:WHITE,false,MUTED)\n ]))\n ]);\n}\n\nfunction kwTbl(){\n const entries=Object.entries(plan.keyword_clusters||{});\n if(!entries.length) return '';\n return tbl(9360,[2400,6960],[\n trow([hCell('Pillar',2400),hCell('Keywords',6960)]),\n ...entries.map(([p,t],i)=>trow([\n cell(p.replace(/_/g,' ').toUpperCase(),2400,LBLUE,true,NAVY),\n cell(Array.isArray(t)?t.join(', '):'',6960,i%2?ALT:WHITE)\n ]))\n ]);\n}\n\nfunction ltkTbl(){\n const ltk=plan.long_tail_keywords||[];\n if(!ltk.length) return '';\n return tbl(9360,[720,8640],[\n trow([hCell('#',720),hCell('Keyword',8640)]),\n ...ltk.map((k,i)=>trow([\n cell(String(i+1),720,i%2?ALT:WHITE,true,BLUE),\n cell(k,8640,i%2?ALT:WHITE)\n ]))\n ]);\n}\n\nlet gNumId=2;\n\nfunction parseMdTable(lines, startIdx){\n const tableLines=[];\n let i=startIdx;\n while(i<lines.length && lines[i].trim().startsWith('|')){\n tableLines.push(lines[i].trim());\n i++;\n }\n if(tableLines.length<2) return {xml:'',endIdx:startIdx+1};\n\n const headerCells=tableLines[0].split('|').map(c=>c.trim()).filter(c=>c.length>0);\n const dataRows=[];\n for(let r=2;r<tableLines.length;r++){\n const cells=tableLines[r].split('|').map(c=>c.trim()).filter(c=>c.length>0);\n if(cells.length>0) dataRows.push(cells);\n }\n\n const numCols=headerCells.length;\n const colW=Math.floor(9360/numCols);\n const cols=Array(numCols).fill(colW);\n cols[numCols-1]=9360-colW*(numCols-1);\n\n const grid=cols.map(w=>`<w:gridCol w:w=\"${w}\"/>`).join('');\n const hdrRow=trow(headerCells.map((c,ci)=>hCell(c.replace(/\\*\\*(.+?)\\*\\*/g,'$1'),cols[ci])));\n const dataRowsXml=dataRows.map((row,ri)=>\n trow(cols.map((_,ci)=>{\n const txt=(row[ci]||'').replace(/\\*\\*(.+?)\\*\\*/g,'$1').replace(/^:?-+:?$/,'');\n return cell(txt,cols[ci],ri%2?ALT:WHITE);\n }))\n );\n const xml=`<w:tbl><w:tblPr><w:tblW w:w=\"9360\" w:type=\"dxa\"/><w:tblLayout w:type=\"fixed\"/><w:tblCellMar><w:top w:w=\"0\" w:type=\"dxa\"/><w:bottom w:w=\"0\" w:type=\"dxa\"/></w:tblCellMar></w:tblPr><w:tblGrid>${grid}</w:tblGrid>${hdrRow}${dataRowsXml.join('')}</w:tbl>`;\n return {xml, endIdx:i};\n}\n\nfunction renderContent(text){\n if(!text) return spacerP();\n const out=[];\n const lines=text.split('\\n');\n let i=0;\n let prevWasNum=false;\n while(i<lines.length){\n const line=lines[i];\n const t=line.trim();\n\n if(t.startsWith('|') && t.endsWith('|') && t.length>2){\n const result=parseMdTable(lines,i);\n if(result.xml){ out.push(spacerP(),result.xml,spacerP()); i=result.endIdx; continue; }\n }\n i++;\n\n if(!t||/^[=\u2500\\-]{10,}$/.test(t)){ continue; }\n if(/^\\d+\\.\\s+[A-Z][A-Z\\s&]+$/.test(t)){ continue; }\n if(/^[{\\[\\]},]/.test(t)){ continue; }\n if(/^\"[a-z_]+\"\\s*:/.test(t)){ continue; }\n if(/^\"[^\"]+\",?$/.test(t) && !t.includes(' \\u2014 ') && !t.includes(': ')){ continue; }\n if(/^\\|[-:\\s|]+\\|$/.test(t)){ continue; }\n if(/^[+\\-]{4,}/.test(t)){ continue; }\n if(/^`{3}/.test(t)){ continue; }\n if(/^[\\s|^v<>+\\-]+$/.test(t) && /[|^v<>+]/.test(t) && !t.includes(':')){ continue; }\n\n if(t.startsWith('#### ')){ out.push(h4(t.slice(5))); continue; }\n if(t.startsWith('### ')){ out.push(h3(t.slice(4))); continue; }\n if(t.startsWith('## ')){ out.push(h2(t.slice(3))); continue; }\n\n if(/^--- (ARTICLE|POST|EMAIL|GUIDE) BREAK ---$/i.test(t)){ out.push(dividerP()); continue; }\n if(/^(ARTICLE|POST|SOCIAL|EMAIL|GUIDE)\\s+\\d+/i.test(t)){\n out.push(h2(t)); prevWasNum=false;\n continue;\n }\n\n if(t.startsWith('Meta description:') || t.startsWith('SEO Title:')){ out.push(mutedP(t)); continue; }\n\n if(t==='Introduction' || t==='Introduction:'){\n out.push(para(run('Introduction',{bold:true,color:BLUE,size:22}),{spaceBefore:120,spaceAfter:40}));\n continue;\n }\n if(t==='Key Takeaways' || t==='Key Takeaways:'){\n out.push(para(run('Key Takeaways',{bold:true,color:NAVY,size:22}),{spaceBefore:160,spaceAfter:60,borderBottom:LBLUE}));\n continue;\n }\n if(t.startsWith('Executive Summary:') || t==='Executive Summary'){\n out.push(para(run('Executive Summary',{bold:true,color:NAVY,size:22}),{spaceBefore:160,spaceAfter:60,borderBottom:LBLUE}));\n continue;\n }\n if(/^(Subject|Preview Text|Preview text):/.test(t)){\n const ci=t.indexOf(':');\n out.push(labelValueP(t.slice(0,ci),t.slice(ci+1).trim()));\n continue;\n }\n if(t.startsWith('Actionable Takeaway:')){\n out.push(takeawayP(t.slice(20).trim()));\n continue;\n }\n if(t.startsWith('Quick Tip:')){\n out.push(`<w:p><w:pPr><w:spacing w:before=\"80\" w:after=\"80\"/><w:shd w:val=\"clear\" w:color=\"auto\" w:fill=\"${LBLUE}\"/></w:pPr>${run('Quick Tip: ',{bold:true,color:NAVY,size:20})}${run(t.slice(10).trim(),{color:CHARCOAL,size:20})}</w:p>`);\n continue;\n }\n if(t.startsWith('How [Your Brand] Can Help') || t.startsWith('How [Your Company Name] Can Help:')){\n out.push(para(run('How [Your Brand] Can Help',{bold:true,color:NAVY,size:22}),{spaceBefore:160,spaceAfter:60,borderBottom:BLUE}));\n continue;\n }\n if(t.startsWith('Recommended Next Steps') || t.startsWith('Recommended Next Steps:')){\n out.push(para(run('Recommended Next Steps',{bold:true,color:NAVY,size:22}),{spaceBefore:160,spaceAfter:60,borderBottom:BLUE}));\n continue;\n }\n\n if(/^\\d+[\\.\\)]\\s/.test(t)){\n if(!prevWasNum){ gNumId++; if(gNumId>51) gNumId=3; }\n prevWasNum=true;\n out.push(numP(t.replace(/^\\d+[\\.\\)]\\s+/,'').replace(/\\*\\*(.+?)\\*\\*/g,'$1'), gNumId));\n continue;\n }\n if(/^[\\*\\-\\u2022]\\s/.test(t)){ out.push(bulletP(t.slice(2).replace(/\\*\\*(.+?)\\*\\*/g,'$1'))); continue; }\n\n if(t.startsWith('#') && t.includes(' ') && !t.startsWith('##')){\n out.push(para(run(t,{color:BLUE,size:20,italic:true}),{spaceBefore:40,spaceAfter:80}));\n continue;\n }\n\n if(/^(Sincerely|Best regards),?$/.test(t)){\n out.push(para(run(t,{color:MUTED,size:22,italic:true}),{spaceBefore:80,spaceAfter:20}));\n continue;\n }\n if(t==='The [Your Brand] Team'){\n out.push(para(run(t,{bold:true,color:NAVY,size:22}),{spaceBefore:0,spaceAfter:80}));\n continue;\n }\n\n prevWasNum=false; out.push(bodyP(t.replace(/\\*\\*(.+?)\\*\\*/g,'$1')));\n }\n return out.join('');\n}\n\nfunction getSection(text, num){\n const parts = text.split(/\\n+={10,}\\n+/);\n for(const part of parts){\n if(part.trim().split('\\n')[0].trim().match(new RegExp('^'+num+'\\\\.\\\\s'))){\n let body = part.trim()\n .replace(/^[^\\n]+\\n+[-\u2500]{5,}[^\\n]*\\n+/, '')\n .trim();\n if(body.startsWith('{') || body.startsWith('\"')){\n body = body.replace(/^[\\s\\S]*?\\}\\s*\\n/, '').trim();\n }\n return body;\n }\n }\n return '';\n}\n\nfunction getStrategy(text){\n const parts = text.split(/\\n+={10,}\\n+/);\n for(const part of parts){\n if(part.trim().split('\\n')[0].trim().startsWith('1. MONTHLY')){\n let body = part.trim()\n .replace(/^[^\\n]+\\n+[-\u2500]{5,}[^\\n]*\\n+/, '')\n .trim();\n body = body.replace(/^[\\s\\S]*?\\}\\s*\\n/, '').trim();\n return body;\n }\n }\n return '';\n}\n\nfunction linkedinSummaryTbl(){\n const posts=plan.linkedin_posts||[];\n if(!posts.length) return '';\n return tbl(9360,[3120,4680,1560],[\n trow([hCell('Topic',3120),hCell('Hook Preview',4680),hCell('Pillar',1560)]),\n ...posts.map((p,i)=>{\n const hook=p.hook||'';\n return trow([\n cell(p.topic||'',3120,i%2?ALT:WHITE),\n cell(hook,4680,i%2?ALT:WHITE,false,BLUE),\n cell((p.content_pillar||'').replace(' & Fluency','').replace(' Systems',''),1560,i%2?ALT:WHITE,false,MUTED)\n ]);\n })\n ]);\n}\nfunction socialSummaryTbl(){\n const posts=plan.social_posts||[];\n if(!posts.length) return '';\n return tbl(9360,[7800,1560],[\n trow([hCell('Topic',7800),hCell('Format',1560)]),\n ...posts.map((p,i)=>trow([\n cell(p.topic||'',7800,i%2?ALT:WHITE),\n cell(p.format||'',1560,i%2?ALT:WHITE,false,BLUE)\n ]))\n ]);\n}\nfunction emailSummaryTbl(){\n const emails=plan.email_topics||[];\n if(!emails.length) return '';\n return tbl(9360,[7200,2160],[\n trow([hCell('Subject Line',7200),hCell('Pillar',2160)]),\n ...emails.map((e,i)=>trow([\n cell(e.subject||'',7200,i%2?ALT:WHITE),\n cell((e.content_pillar||'').replace(' & Fluency','').replace(' Systems',''),2160,i%2?ALT:WHITE,false,BLUE)\n ]))\n ]);\n}\nfunction guidesSummaryTbl(){\n const guides=plan.guides||[];\n if(!guides.length) return '';\n return tbl(9360,[7200,2160],[\n trow([hCell('Guide Title',7200),hCell('Pillar',2160)]),\n ...guides.map((g,i)=>trow([\n cell(g.title||'',7200,i%2?ALT:WHITE),\n cell((g.content_pillar||'').replace(' & Fluency','').replace(' Systems',''),2160,i%2?ALT:WHITE,false,BLUE)\n ]))\n ]);\n}\n\nconst bodyXml=[\n cover(),\n h1('Table of Contents'), tocTbl(), dividerP(),\n h1('1. Monthly Content Strategy Overview'),\n `<w:p><w:pPr><w:spacing w:before=\"80\" w:after=\"80\"/><w:shd w:val=\"clear\" w:color=\"auto\" w:fill=\"${LBLUE}\"/></w:pPr>${run('Strategy Overview',{bold:true,color:NAVY,size:22})}</w:p>`,\n bodyP(plan.strategy_overview||''),\n spacerP(),\n dividerP(),\n h1('2. SEO Blog Articles'),\n blogSummaryTbl(), dividerP(),\n renderContent(getSection(assembled,'2')), dividerP(),\n h1('3. LinkedIn Thought Leadership Posts'),\n linkedinSummaryTbl(), dividerP(),\n renderContent(getSection(assembled,'3')), dividerP(),\n h1('4. Educational Social Media Posts'),\n socialSummaryTbl(), dividerP(),\n renderContent(getSection(assembled,'4')), dividerP(),\n h1('5. Email Newsletter Campaigns'),\n emailSummaryTbl(), dividerP(),\n renderContent(getSection(assembled,'5')), dividerP(),\n h1('6. AI Strategy & Automation Guides'),\n guidesSummaryTbl(), dividerP(),\n renderContent(getSection(assembled,'6')), dividerP(),\n h1('7. Keyword Clusters & Long-Tail Keywords'),\n h2('Keyword Clusters'), kwTbl(),\n h2('Long-Tail Keywords'), ltkTbl(),\n].join('');\n\nconst xmlFiles={\n'[Content_Types].xml':'<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Types xmlns=\"http://schemas.openxmlformats.org/packag
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
facebookGraphApigmailOAuth2googleAnalyticsOAuth2googleDriveOAuth2ApigooglePalmApihttpBasicAuthlinkedInOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow is an end-to-end, enterprise-grade content engine. It automates the entire lifecycle of a brand's content marketing: capturing business context via an n8n Form Trigger to generate a master strategy document, using real-time analytics to write 34 targeted assets,…
Source: https://n8n.io/workflows/15327/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
✨🤖Automated AI Powered Social Media Content Factory for X + Facebook + Instagram + LinkedIn. Uses outputParserStructured, lmChatGoogleGemini, lmChatOpenAi, httpRequest. Event-driven trigger; 57 nodes
Social Media Managers and Digital Marketers seeking to streamline content production across 7+ platforms (X/Twitter, Instagram, LinkedIn, Facebook, TikTok, Threads, YouTube Shorts) using AI-powered au
This workflow is designed for content creators, social media managers, digital marketers, and business owners who want to automate their content creation and distribution process across multiple platf
✨🩷Automated Social Media Content Publishing Factory + System Prompt Composition. Uses chatTrigger, stickyNote, toolWorkflow, memoryBufferWindow. Chat trigger; 100 nodes.
This workflow is designed for content creators, social media managers, and marketing teams who need to efficiently create and publish content across multiple social media platforms. It's perfect for b