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 →
{
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtDay": [
1,
4
],
"triggerAtHour": 8
}
]
}
},
"id": "a2ab0403-cf77-4f0f-a20b-67460c6e795a",
"name": "\u23f0 Schedule (Lun/Jue 8am)",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
864,
480
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "a2",
"name": "devto_api_key",
"value": "{{ DEVTO_API_KEY }}",
"type": "string"
},
{
"id": "a3",
"name": "supabase_service_key",
"value": "{{ SUPABASE_SERVICE_ROLE_KEY }}",
"type": "string"
},
{
"id": "a6",
"name": "github_owner",
"value": "Mgobeaalcoba",
"type": "string"
},
{
"id": "a7",
"name": "github_repo",
"value": "Mgobeaalcoba.github.io",
"type": "string"
}
]
},
"options": {}
},
"id": "2a4f3214-8c2e-4e12-a1f2-1289f1af9d1f",
"name": "\u2699\ufe0f Configuraci\u00f3n",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1088,
480
]
},
{
"parameters": {
"jsCode": "\nconst xml = $input.item.json.data || $input.item.json;\nconst xmlStr = typeof xml === 'string' ? xml : JSON.stringify(xml);\n\n// Extract <item> blocks\nconst items = [];\nconst itemRegex = /<item[^>]*>([\\s\\S]*?)<\\/item>/gi;\nlet match;\nwhile ((match = itemRegex.exec(xmlStr)) !== null && items.length < 6) {\n const block = match[1];\n const get = (tag) => {\n const m = block.match(new RegExp('<' + tag + '[^>]*><!\\\\[CDATA\\\\[([\\\\s\\\\S]*?)\\\\]\\\\]><\\/' + tag + '>|<' + tag + '[^>]*>([^<]*)<\\/' + tag + '>', 'i'));\n return m ? (m[1] || m[2] || '').trim() : '';\n };\n items.push({\n title: get('title'),\n description: get('description'),\n link: get('link'),\n pubDate: get('pubDate')\n });\n}\n\n// Filter relevant to data/AI/engineering\nconst keywords = ['ai', 'machine learning', 'data', 'model', 'llm', 'gemini', 'python', 'cloud', 'automation', 'engineering'];\nconst scored = items.map(item => {\n const text = (item.title + ' ' + item.description).toLowerCase();\n const score = keywords.reduce((s, kw) => s + (text.includes(kw) ? 1 : 0), 0);\n return { ...item, score };\n}).sort((a, b) => b.score - a.score);\n\nconst selected = scored[0];\nif (!selected || !selected.title) throw new Error('No relevant RSS items found');\n\nreturn {\n topic_title: selected.title,\n topic_description: selected.description,\n topic_link: selected.link,\n topic_date: selected.pubDate,\n source: 'Hacker News'\n};\n"
},
"id": "de503336-c295-464a-8479-cacb9b9fe444",
"name": "\ud83d\udd27 Parsear RSS",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1536,
480
]
},
{
"parameters": {
"jsCode": "const config = $('\u2699\ufe0f Configuraci\u00f3n').item.json;\nconst topic = $('\ud83d\udd27 Parsear RSS').item.json;\nconst art = $input.item.json; // ya viene parseado\n\nconst today = new Date().toISOString().split('T')[0];\nconst filename = `${today}-${art.output.slug}.md`;\n\nconst fileContent = `---\\nslug: ${art.slug}\\ndate: ${today}\\n---\\n\\n${art.content_markdown_es}\\n`;\n\nreturn {\n ...art,\n filename,\n date: today,\n github_file_path: `cv/content/posts/${filename}`,\n github_file_content_base64: Buffer.from(fileContent, 'utf8').toString('base64'),\n canonical_url: `https://mgobeaalcoba.github.io/blog/${art.output.slug}/`,\n github_pat: config.github_pat,\n devto_api_key: config.devto_api_key,\n supabase_service_key: config.supabase_service_key\n};"
},
"id": "b1d8d91f-0dc4-4631-9fbd-dc36d4a0c857",
"name": "\ud83d\udd27 Parsear Art\u00edculo",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2256,
480
]
},
{
"parameters": {
"method": "POST",
"url": "{{ SUPABASE_URL }}/rest/v1/blog_posts",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.supabase_service_key }}"
},
{
"name": "Authorization",
"value": "=Bearer {{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.supabase_service_key }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=representation"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"slug\": \"{{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.slug }}\",\n \"file\": \"{{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.filename }}\",\n \"title_es\": {{ JSON.stringify($('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.title_es) }},\n \"title_en\": {{ JSON.stringify($('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.title_en) }},\n \"excerpt_es\": {{ JSON.stringify($('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.excerpt_es) }},\n \"excerpt_en\": {{ JSON.stringify($('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.excerpt_en) }},\n \"date\": \"{{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.date }}\",\n \"category\": \"{{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.category }}\",\n \"featured\": false,\n \"read_time\": \"{{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.read_time }}\",\n \"author\": \"Mariano Gobea Alcoba\",\n \"sort_order\": 99\n}",
"options": {}
},
"id": "78caa053-7b8f-4139-86d5-24af872f5a78",
"name": "\ud83d\udcca Supabase: Insert Post",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2480,
480
]
},
{
"parameters": {
"jsCode": "\nconst art = $('\ud83d\udd27 Parsear Art\u00edculo').item.json.output;\nconst supabaseResult = $input.item.json;\n\n// supabaseResult is the newly created blog_post row\nconst newPost = Array.isArray(supabaseResult) ? supabaseResult[0] : supabaseResult;\nconst postId = newPost?.id;\nif (!postId) throw new Error('Supabase insert did not return an ID. Response: ' + JSON.stringify(supabaseResult));\n\n// Prepare tags for batch insert\nconst tagsPayload = art.tags.map(tag => ({\n post_id: postId,\n tag: tag.toLowerCase().replace(/[^a-z0-9]/g, '')\n})).filter(t => t.tag.length > 0);\n\nreturn {\n post_id: postId,\n tags_payload: tagsPayload,\n tags_payload_json: JSON.stringify(tagsPayload),\n slug: art.slug,\n title_en: art.title_en,\n canonical_url: art.canonical_url,\n content_markdown_en: art.content_markdown_en,\n tags: art.tags,\n devto_api_key: art.devto_api_key,\n supabase_service_key: art.supabase_service_key\n};\n"
},
"id": "e8a419f2-9939-4da5-82e3-8c99253c6811",
"name": "\ud83d\udd27 Preparar Tags",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2704,
480
]
},
{
"parameters": {
"method": "POST",
"url": "{{ SUPABASE_URL }}/rest/v1/blog_post_tags",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('\u2699\ufe0f Configuraci\u00f3n').item.json.supabase_service_key }}"
},
{
"name": "Authorization",
"value": "=Bearer {{ $('\u2699\ufe0f Configuraci\u00f3n').item.json.supabase_service_key }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.tags_payload_json }}",
"options": {}
},
"id": "00c491f6-1ed2-4363-a889-d7f026fd0e64",
"name": "\ud83d\udcca Supabase: Insert Tags",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2928,
480
]
},
{
"parameters": {
"method": "POST",
"url": "https://dev.to/api/articles",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "api-key",
"value": "={{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.devto_api_key }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"article\": {\n \"title\": {{ JSON.stringify($('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.title_en + \"!\") }},\n \"body_markdown\": {{ JSON.stringify($('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.content_markdown_en + \"\\n\\n---\\n*Originally published in Spanish at [mgobeaalcoba.github.io/blog/\" + $('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.slug + \"/](\" + $('\ud83d\udd27 Parsear Art\u00edculo').item.json.canonical_url + \")*\") }},\n \"published\": true,\n \"canonical_url\": {{ JSON.stringify($('\ud83d\udd27 Parsear Art\u00edculo').item.json.canonical_url) }},\n \"tags\": {{ JSON.stringify($('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.tags.map(t => t.toLowerCase().replace(/[^a-z0-9]/g, '')).slice(0,4)) }},\n \"series\": \"Data Engineering in the Trenches\"\n }\n}",
"options": {}
},
"id": "77367945-3914-4229-969b-594d4ac21285",
"name": "\ud83d\ude80 dev.to: Publicar",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3376,
480
]
},
{
"parameters": {
"url": "https://hnrss.org/frontpage",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
}
},
"id": "4311c735-a69c-4e00-b9cc-d82daa434feb",
"name": "Hacker News RSS",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1312,
480
]
},
{
"parameters": {
"promptType": "define",
"text": "={{ $json.topic_title }}\n{{ $json.topic_description }}\n{{ $json.topic_link }}\n{{ $json.topic_date }}\n{{ $json.source }}",
"hasOutputParser": true,
"options": {
"systemMessage": "You are an expert technical writer specializing in Data Engineering, AI, Python, and Big Data. Your audience is software developers and data professionals in Latin America and globally.\n\nYour task is to write a COMPLETE technical blog post based on a trending topic provided by the user (from Google AI Blog RSS feed).\n\nThe article must be original, not a translation or summary of the source \u2014 use the topic as INSPIRATION to write something practical and actionable for data engineers and developers.\n\nAlways return your response as a single valid JSON object with this EXACT structure \u2014 no markdown wrapper, no explanation, just the raw JSON:\n\n{\n \"slug\": \"url-friendly-english-slug-max-6-words\",\n \"title_es\": \"T\u00edtulo atractivo en espa\u00f1ol (max 70 caracteres)\",\n \"title_en\": \"Attractive English title (max 70 characters)\",\n \"excerpt_es\": \"Resumen atractivo en espa\u00f1ol de 150-180 caracteres que invite a leer el art\u00edculo completo\",\n \"excerpt_en\": \"Attractive English summary of 150-180 characters that invites reading the full article\",\n \"category\": \"one of exactly: data-engineering, machine-learning, ai-tools, python, bigquery\",\n \"tags\": [\"tag1\", \"tag2\", \"tag3\", \"tag4\"],\n \"read_time\": \"X min\",\n \"content_markdown_es\": \"FULL article in Spanish (1500-2500 words). Start directly with content paragraphs \u2014 no frontmatter, no title heading at the top. Use ## for sections, ### for subsections, code blocks with triple backticks and language tag. Include real-world examples. End with a paragraph inviting readers to visit https://mgobeaalcoba.github.io/consulting/ for consulting services.\",\n \"content_markdown_en\": \"FULL article in English (1500-2500 words). Same structure and depth as the Spanish version but naturally written in English for a global developer audience \u2014 not a literal translation.\"\n}\n\nRules:\n- slug: lowercase, hyphens only, no special characters, descriptive of the content\n- tags: lowercase, no spaces, no special characters (e.g. python, bigquery, llm, dataengineering)\n- category: must be exactly one of the five options listed\n- content_markdown fields: must be complete articles with real depth, not summaries or outlines\n- Return ONLY the JSON object \u2014 nothing before or after it"
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
1792,
480
],
"id": "43965d7e-2db4-47c5-9a3f-ed7171fb4e07",
"name": "AI Agent"
},
{
"parameters": {
"model": "z-ai/glm-4.5-air:free",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"typeVersion": 1,
"position": [
1760,
704
],
"id": "aad66f26-4317-401c-8bc8-018e093264ca",
"name": "OpenRouter Chat Model",
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"slug\": {\n \"type\": \"string\",\n \"description\": \"URL-friendly English slug, max 6 words, lowercase, hyphens only\"\n },\n \"title_es\": {\n \"type\": \"string\",\n \"description\": \"T\u00edtulo atractivo en espa\u00f1ol, m\u00e1ximo 70 caracteres\"\n },\n \"title_en\": {\n \"type\": \"string\",\n \"description\": \"Attractive English title, maximum 70 characters\"\n },\n \"excerpt_es\": {\n \"type\": \"string\",\n \"description\": \"Resumen en espa\u00f1ol de 150-180 caracteres que invite a leer el art\u00edculo\"\n },\n \"excerpt_en\": {\n \"type\": \"string\",\n \"description\": \"English summary of 150-180 characters that invites reading the full article\"\n },\n \"category\": {\n \"type\": \"string\",\n \"enum\": [\"data-engineering\", \"machine-learning\", \"ai-tools\", \"python\", \"bigquery\"],\n \"description\": \"Article category, must be exactly one of the enum values\"\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": { \"type\": \"string\" },\n \"minItems\": 2,\n \"maxItems\": 4,\n \"description\": \"Lowercase tags, no spaces, no special characters\"\n },\n \"read_time\": {\n \"type\": \"string\",\n \"description\": \"Estimated read time, e.g. '8 min'\"\n },\n \"content_markdown_es\": {\n \"type\": \"string\",\n \"description\": \"Full article in Spanish, 1500-2500 words, markdown format, no frontmatter\"\n },\n \"content_markdown_en\": {\n \"type\": \"string\",\n \"description\": \"Full article in English, 1500-2500 words, markdown format, no frontmatter\"\n }\n },\n \"required\": [\n \"slug\",\n \"title_es\",\n \"title_en\",\n \"excerpt_es\",\n \"excerpt_en\",\n \"category\",\n \"tags\",\n \"read_time\",\n \"content_markdown_es\",\n \"content_markdown_en\"\n ]\n}",
"autoFix": true
},
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"typeVersion": 1.3,
"position": [
1888,
704
],
"id": "6f67e7c0-0def-49b3-a087-78815bdbc31d",
"name": "Structured Output Parser"
},
{
"parameters": {
"model": "z-ai/glm-4.5-air:free",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"typeVersion": 1,
"position": [
1968,
912
],
"id": "c530fa46-51eb-44bc-93f3-cba97ebdf483",
"name": "OpenRouter Chat Model1",
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"authentication": "oAuth2",
"resource": "file",
"owner": {
"__rl": true,
"value": "={{ $('\u2699\ufe0f Configuraci\u00f3n').item.json.github_owner }}",
"mode": "name"
},
"repository": {
"__rl": true,
"value": "={{ $('\u2699\ufe0f Configuraci\u00f3n').item.json.github_repo }}",
"mode": "name"
},
"filePath": "={{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.github_file_path }}",
"fileContent": "={{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.content_markdown_es }}",
"commitMessage": "=Delete {{ $('\ud83d\udd27 Parsear Art\u00edculo').item.json.output.slug }} to cv/content/posts"
},
"type": "n8n-nodes-base.github",
"typeVersion": 1.1,
"position": [
3152,
480
],
"id": "d45dfa7b-4c5b-42ab-ab6c-dfec84cb57c7",
"name": "Create a file",
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
}
}
],
"connections": {
"\u23f0 Schedule (Lun/Jue 8am)": {
"main": [
[
{
"node": "\u2699\ufe0f Configuraci\u00f3n",
"type": "main",
"index": 0
}
]
]
},
"\u2699\ufe0f Configuraci\u00f3n": {
"main": [
[
{
"node": "Hacker News RSS",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd27 Parsear RSS": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd27 Parsear Art\u00edculo": {
"main": [
[
{
"node": "\ud83d\udcca Supabase: Insert Post",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcca Supabase: Insert Post": {
"main": [
[
{
"node": "\ud83d\udd27 Preparar Tags",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd27 Preparar Tags": {
"main": [
[
{
"node": "\ud83d\udcca Supabase: Insert Tags",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcca Supabase: Insert Tags": {
"main": [
[
{
"node": "Create a file",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\ude80 dev.to: Publicar": {
"main": [
[]
]
},
"Hacker News RSS": {
"main": [
[
{
"node": "\ud83d\udd27 Parsear RSS",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "\ud83d\udd27 Parsear Art\u00edculo",
"type": "main",
"index": 0
}
]
]
},
"OpenRouter Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "AI Agent",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"OpenRouter Chat Model1": {
"ai_languageModel": [
[
{
"node": "Structured Output Parser",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Create a file": {
"main": [
[
{
"node": "\ud83d\ude80 dev.to: Publicar",
"type": "main",
"index": 0
}
]
]
}
},
"meta": {
"templateCredsSetupCompleted": true
},
"_documentation": {
"name": "AI Blog Creator",
"version": "1.0",
"description": "Reads trending topics from Hacker News RSS, generates bilingual technical articles with AI (OpenRouter), publishes to GitHub (triggers site rebuild), inserts metadata in Supabase, and publishes as draft on dev.to.",
"trigger": "Schedule: Mon/Thu 8am + Manual",
"rss_source": "Hacker News Frontpage (https://hnrss.org/frontpage)",
"ai_model": "OpenRouter (z-ai/glm-4.5-air:free or configured model)",
"placeholders_to_fill": [
"{{ DEVTO_API_KEY }} \u2014 dev.to Settings \u2192 Extensions \u2192 API Keys",
"{{ SUPABASE_SERVICE_ROLE_KEY }} \u2014 Supabase Dashboard \u2192 Settings \u2192 API \u2192 Legacy \u2192 service_role",
"{{ SUPABASE_URL }} \u2014 Supabase Dashboard \u2192 Settings \u2192 API \u2192 Project URL",
"{{ CREDENTIAL_ID }} \u2014 Auto-assigned by n8n when credentials are configured"
],
"credentials_required": [
"OpenRouter account (n8n credential type: OpenRouter API)",
"Mgobeaalcoba Github (n8n credential type: GitHub OAuth2)"
]
}
}
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.
githubOAuth2ApiopenRouterApi
About this workflow
01-Ai-Blog-Creator. Uses httpRequest, agent, lmChatOpenRouter, outputParserStructured. Scheduled trigger; 14 nodes.
Source: https://github.com/Mgobeaalcoba/Mgobeaalcoba.github.io/blob/main/docs/automations/01-ai-blog-creator.json — original creator credit. Request a take-down →