This workflow corresponds to n8n.io template #8191 — we link there as the canonical source.
This workflow follows the Form Trigger → Google Docs 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": "hCUgw7o0NPlNWR0K",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Form to Blog Automation using Dumpling AI and Google Docs",
"tags": [],
"nodes": [
{
"id": "5e40b2b3-8d03-46b0-b1da-a1111997da55",
"name": "Form Submission (Keywords)",
"type": "n8n-nodes-base.formTrigger",
"position": [
-416,
32
],
"parameters": {
"options": {},
"formTitle": "Blog form",
"formFields": {
"values": [
{
"fieldLabel": "Keywords"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "4e2c4c76-6d22-4a6b-abd1-8771b2ff21f7",
"name": "Dumpling AI Autocomplete",
"type": "n8n-nodes-base.httpRequest",
"position": [
-192,
32
],
"parameters": {
"url": "https://app.dumplingai.com/api/v1/get-autocomplete",
"method": "POST",
"options": {},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $json.Keywords }}"
},
{
"name": "country",
"value": "US"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "99e4b141-da82-4131-a38e-d65e14307049",
"name": "Split Autocomplete Suggestions",
"type": "n8n-nodes-base.splitOut",
"position": [
32,
32
],
"parameters": {
"options": {},
"fieldToSplitOut": "suggestions"
},
"typeVersion": 1
},
{
"id": "cc647e9e-fb05-4ea0-ac9c-c5c619ff0d7c",
"name": "Loop Suggestions",
"type": "n8n-nodes-base.splitInBatches",
"position": [
256,
32
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "dd3edb69-ea7c-4d45-9fec-28400e9c16d7",
"name": "Delay Between Requests",
"type": "n8n-nodes-base.wait",
"position": [
464,
32
],
"parameters": {},
"typeVersion": 1.1
},
{
"id": "2e768d0f-a2fa-4236-a25f-1ed2ae36a9f1",
"name": "Dumpling AI Google News",
"type": "n8n-nodes-base.httpRequest",
"position": [
672,
32
],
"parameters": {
"url": " https://app.dumplingai.com/api/v1/search-news",
"method": "POST",
"options": {},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $json.value }}"
},
{
"name": "country",
"value": "US"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "4cc5e45e-3fd1-444b-b872-83ca62196b41",
"name": "Split News Articles",
"type": "n8n-nodes-base.splitOut",
"position": [
864,
32
],
"parameters": {
"options": {},
"fieldToSplitOut": "news"
},
"typeVersion": 1
},
{
"id": "3f03dc09-2cbc-4c6a-a798-1a61b81a8073",
"name": "Filter Articles (1\u20132 Days Old)",
"type": "n8n-nodes-base.code",
"position": [
1104,
32
],
"parameters": {
"jsCode": "// Works in n8n Code node\n// Filters only articles that are between 1 and 2 days old\n\nfunction parseRelative(relative, baseNow) {\n if (typeof relative !== 'string') return null;\n const s = relative.trim().toLowerCase();\n\n // quick rejects\n if (s.includes('yesterday')) {\n // treat \"yesterday\" as exactly 1 day ago\n return new Date(baseNow.getTime() - 24 * 3600000);\n }\n if (s.includes('week') || s.includes('month') || s.includes('year')) return null;\n\n // match \"15 hours ago\", \"2 days ago\", etc.\n const m = s.match(/(\\d+)\\s*(second|seconds|sec|secs|minute|min|minutes|mins|hour|hours|day|days)\\s*ago$/);\n if (!m) return null;\n\n const amount = parseInt(m[1], 10);\n const unit = m[2];\n\n const msMap = {\n second: 1000, seconds: 1000, sec: 1000, secs: 1000,\n minute: 60000, minutes: 60000, min: 60000, mins: 60000,\n hour: 3600000, hours: 3600000,\n day: 86400000, days: 86400000,\n };\n\n const ms = msMap[unit];\n if (!ms) return null;\n\n return new Date(baseNow.getTime() - amount * ms);\n}\n\n// Collect articles from either shape\nlet articles = [];\nif (items.length === 1 && Array.isArray(items[0].json)) {\n articles = items[0].json;\n} else {\n articles = items.map(i => i.json);\n}\n\n// Determine \"now\" (or override with present_date if provided)\nlet baseNow = new Date();\nconst present = items?.[0]?.json?.present_date;\nif (typeof present === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(present)) {\n const [y, m, d] = present.split('-').map(n => parseInt(n, 10));\n baseNow = new Date(y, m - 1, d, 12, 0, 0, 0);\n}\n\n// Define boundaries\nconst twoDaysAgo = new Date(baseNow.getTime() - 2 * 24 * 3600000);\nconst oneDayAgo = new Date(baseNow.getTime() - 1 * 24 * 3600000);\n\nconst out = [];\nfor (const a of articles) {\n const rel = a.date || a.published_time || a.publishedAt || a.pubDate || a.time || '';\n const abs = parseRelative(rel, baseNow);\n if (!abs) continue;\n\n // Keep only if between 1 and 2 days ago\n if (abs >= twoDaysAgo && abs < oneDayAgo) {\n const url = a.link || a.url || a.uri;\n if (!url) continue;\n\n out.push({ json: { URL: String(url), score: 10 } });\n }\n}\n\nreturn out;\n"
},
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "a62c05a4-a000-4e0a-af8d-834f0623802c",
"name": "Limit Articles",
"type": "n8n-nodes-base.limit",
"position": [
1328,
32
],
"parameters": {
"maxItems": 2
},
"typeVersion": 1
},
{
"id": "30ba63a3-5ac6-4a80-ab09-39ead3320774",
"name": "Dumpling AI Scraper",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
1568,
32
],
"parameters": {
"url": "https://app.dumplingai.com/api/v1/scrape",
"method": "POST",
"options": {},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "url",
"value": "={{ $json.URL }}"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "f1ad7937-7877-4c35-9840-27b8ac21d447",
"name": "Clean & Prepare Article Content",
"type": "n8n-nodes-base.code",
"position": [
1776,
32
],
"parameters": {
"jsCode": "// n8n Code node, JavaScript\n\n// If the incoming item is a single item whose .json is an array,\n// make it our working array. Otherwise assume multiple items.\nconst raw = Array.isArray(items[0]?.json) ? items[0].json : items.map(i => i.json);\n\nfunction cleanMarkdown(md) {\n if (!md || typeof md !== \"string\") return \"\";\n\n let text = md;\n\n // Remove markdown images  and bare images \n text = text.replace(/!\\[[^\\]]*?\\]\\([^)]+\\)/g, \"\");\n\n // Replace markdown links [text](url) with just \"text\"\n text = text.replace(/\\[([^\\]]+)\\]\\((?:https?:\\/\\/|mailto:)[^)]+\\)/g, \"$1\");\n\n // Remove bare URLs\n text = text.replace(/https?:\\/\\/\\S+/g, \"\");\n\n // Cut everything after non article sections\n const cutMarkers = [\n /^##\\s*More videos\\b/mi,\n /^##\\s*Related Articles\\b/mi,\n /^##\\s*More Regional News\\b/mi,\n /^\\s*Share This Article\\b/mi,\n /^##\\s*About Me\\b/mi,\n ];\n for (const rx of cutMarkers) {\n const idx = text.search(rx);\n if (idx !== -1) {\n text = text.slice(0, idx);\n break;\n }\n }\n\n // Drop common clutter lines\n const dropStarts = [\n \"Skip to content\",\n \"Skip to main content\",\n \"Share\",\n \"Watch later\",\n \"Copy link\",\n \"Include playlist\",\n \"More videos\",\n \"You are signed out\",\n \"CancelConfirm\",\n \"Search\",\n \"Shopping\",\n \"Info\",\n \"Tap to unmute\",\n \"If playback does not begin\",\n \"Video Player is loading\",\n \"Play Video\",\n \"Stream Type\",\n \"Seek to live\",\n \"Remaining Time\",\n \"Playback Rate\",\n \"Chapters\",\n \"Captions\",\n \"Audio Track\",\n \"Fullscreen\",\n \"Close Modal Dialog\",\n \"Topic:\",\n ];\n text = text\n .split(\"\\n\")\n .filter(line => {\n const L = line.trim();\n if (!L) return true;\n return !dropStarts.some(s => L.startsWith(s));\n })\n .join(\"\\n\");\n\n // Remove heading markers but keep their words\n text = text.replace(/^#{1,6}\\s*/gm, \"\");\n\n // Remove blockquote markers\n text = text.replace(/^\\s*>\\s?/gm, \"\");\n\n // Remove leftover image captions such as \"(Image by ...)\"\n text = text.replace(/\\((?:Image by|Photo|GIF|Video)[^)]+\\)\\s*/gi, \"\");\n\n // Compress extra blank lines\n text = text.replace(/\\n{3,}/g, \"\\n\\n\").trim();\n\n return text;\n}\n\nconst out = raw.map(a => {\n const title = a.title || \"\";\n const content = a.content || \"\";\n return {\n json: {\n title,\n article: cleanMarkdown(content),\n },\n };\n});\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "e122251c-5540-4dd3-be3f-59620c1aacf2",
"name": "Aggregate Articles",
"type": "n8n-nodes-base.aggregate",
"position": [
496,
-208
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "article",
"destinationFieldName": "article"
},
"typeVersion": 1
},
{
"id": "acf43fa6-f8d4-4533-8522-68640916877c",
"name": "OpenAI: Generate Blog Draft",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
752,
-208
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini",
"cachedResultName": "GPT-4.1-MINI"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "=You are a professional content writer. I will provide you with raw scraped content from different articles. Your job is to create a polished, original blog post based on the ideas and insights in that content.\n\nOutput Requirements:\n- Return ONLY valid JSON (no explanations, no markdown formatting outside JSON, no comments).\n- JSON must have exactly these keys:\n {\n \"Blog_post\": \"Markdown formatted blog post\",\n \"title\": \"A clear, engaging blog post title\"\n }\n\nWriting Instructions:\n1. Write the blog post in engaging, professional, and conversational language.\n2. Use Markdown formatting: \n - Headings (#, ##, ###) for sections\n - Bold or italic where necessary\n - Lists where it improves readability\n3. Do not copy the scraped text directly. Rewrite it in your own words while keeping the meaning.\n4. Exclude clutter such as \u201crelated articles,\u201d \u201cshare this,\u201d or video references.\n5. Organize with a strong introduction, clear body sections, and a compelling conclusion.\n6. Add smooth transitions between sections so the blog post flows naturally.\n\n"
},
{
"content": "=---\nHere is the scraped content to base the blog post on:\n{{ JSON.stringify($json.article) }}\n---\n"
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "04ee4323-3527-401c-a963-a726c7f1dad6",
"name": "Google Docs: Create Blog File",
"type": "n8n-nodes-base.googleDocs",
"position": [
1104,
-208
],
"parameters": {
"title": "={{ $json.message.content.title }}",
"folderId": "1NU00YbKNiHJptNuQZH6kgVUhLvDzE0ka"
},
"credentials": {
"googleDocsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "bacf9175-722d-4f93-a571-4a5e4e555995",
"name": "Google Docs: Insert Blog Content",
"type": "n8n-nodes-base.googleDocs",
"position": [
1312,
-208
],
"parameters": {
"actionsUi": {
"actionFields": [
{
"text": "={{ $('OpenAI: Generate Blog Draft').item.json.message.content.Blog_post }}",
"action": "insert"
}
]
},
"operation": "update",
"documentURL": "={{ $json.id }}"
},
"credentials": {
"googleDocsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "cac11f57-cf15-40ec-9c0a-49ad02b48564",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-400,
-208
],
"parameters": {
"width": 624,
"height": 368,
"content": "## Agent Branch\n\nThis branch starts with a form submission. \nThe entered keyword is expanded using **Dumpling AI Autocomplete**, then news articles are fetched with **Dumpling AI Google News**. \n\nThe workflow splits suggestions and articles, filters them to keep only fresh items (1\u20132 days old), and prepares them for content creation.\n"
},
"typeVersion": 1
},
{
"id": "d382d3f0-74fc-47d3-8b32-8e3c48f5bf40",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
704,
-384
],
"parameters": {
"width": 752,
"height": 256,
"content": "## Blog Branch\n\nFrom each filtered article, the workflow scrapes and cleans content with **Dumpling AI Scraper**. \nThe articles are aggregated and passed to **OpenAI**, which generates a polished blog draft. \n\nThe final post is saved into **Google Docs**, creating and updating a document with the finished blog content.\n"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "34bb81b5-d086-4f95-b702-3f42cb606281",
"connections": {
"Limit Articles": {
"main": [
[
{
"node": "Dumpling AI Scraper",
"type": "main",
"index": 0
}
]
]
},
"Loop Suggestions": {
"main": [
[
{
"node": "Aggregate Articles",
"type": "main",
"index": 0
}
],
[
{
"node": "Delay Between Requests",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Articles": {
"main": [
[
{
"node": "OpenAI: Generate Blog Draft",
"type": "main",
"index": 0
}
]
]
},
"Dumpling AI Scraper": {
"main": [
[
{
"node": "Clean & Prepare Article Content",
"type": "main",
"index": 0
}
]
]
},
"Split News Articles": {
"main": [
[
{
"node": "Filter Articles (1\u20132 Days Old)",
"type": "main",
"index": 0
}
]
]
},
"Delay Between Requests": {
"main": [
[
{
"node": "Dumpling AI Google News",
"type": "main",
"index": 0
}
]
]
},
"Dumpling AI Google News": {
"main": [
[
{
"node": "Split News Articles",
"type": "main",
"index": 0
}
]
]
},
"Dumpling AI Autocomplete": {
"main": [
[
{
"node": "Split Autocomplete Suggestions",
"type": "main",
"index": 0
}
]
]
},
"Form Submission (Keywords)": {
"main": [
[
{
"node": "Dumpling AI Autocomplete",
"type": "main",
"index": 0
}
]
]
},
"OpenAI: Generate Blog Draft": {
"main": [
[
{
"node": "Google Docs: Create Blog File",
"type": "main",
"index": 0
}
]
]
},
"Google Docs: Create Blog File": {
"main": [
[
{
"node": "Google Docs: Insert Blog Content",
"type": "main",
"index": 0
}
]
]
},
"Split Autocomplete Suggestions": {
"main": [
[
{
"node": "Loop Suggestions",
"type": "main",
"index": 0
}
]
]
},
"Clean & Prepare Article Content": {
"main": [
[
{
"node": "Loop Suggestions",
"type": "main",
"index": 0
}
]
]
},
"Filter Articles (1\u20132 Days Old)": {
"main": [
[
{
"node": "Limit Articles",
"type": "main",
"index": 0
}
]
]
}
}
}
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.
googleDocsOAuth2ApihttpHeaderAuthopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow is perfect for content marketers, bloggers, SEO professionals, and virtual assistants who need to transform keyword research into complete blog posts without spending hours writing and formatting.
Source: https://n8n.io/workflows/8191/ — 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.
Transform blog posts, YouTube videos, or any text into LinkedIn posts, Twitter threads, email newsletters, and more with GPT-5.1 Content creators who want to maximize reach from every piece of content
In this tutorial, I’ll walk you through a step-by-step N8N workflow that combines the power of OpenAI and Claude AI to generate professional, ready-to-use lead magnet plans for any niche.
Note: Now includes an Apify alternative for Rapid API (Some users can't create new accounts on Rapid API, so I have added an alternative for you. But immediately you are able to get access to Rapid AP
This system automates LinkedIn lead generation and enrichment in six clear stages: Lead Collection (via Apollo.io) Automatically pulls leads based on keywords, roles, or industries using Apollo’s API.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.