This workflow corresponds to n8n.io template #10542 — we link there as the canonical source.
This workflow follows the Datatable → 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": "01ZGpwv1rdsk5Ng8",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Internal linking shopify blog",
"tags": [],
"nodes": [
{
"id": "8848b159-986e-4b67-8bde-a38da0a25f36",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-1808,
496
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtHour": 20
}
]
}
},
"typeVersion": 1.2
},
{
"id": "057f8499-694c-4391-80a1-e2f7ca093d25",
"name": "Workflow Configuration",
"type": "n8n-nodes-base.set",
"position": [
-1584,
496
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "566ed7c6-84cf-4fcf-ae6e-5607fa4c3909",
"name": "shopifyBlogId",
"type": "string",
"value": ""
},
{
"id": "bdcbb25a-eba1-4a26-bc4e-e4dd8eb268db",
"name": "shopifyBlogDomain",
"type": "string",
"value": "<https://example.com/blogs/>"
},
{
"id": "6f1aaf1c-eb2f-42ac-b16e-6e4cd8cabd9f",
"name": "shopifyStoreName",
"type": "string",
"value": "<domain_before_myshopify.com>"
},
{
"id": "d704d6dd-d887-40d3-9f3b-1a61010095d2",
"name": "shopApiVersion",
"type": "string",
"value": "2025-07"
},
{
"id": "027f4123-2194-4f6d-8411-1305e7602692",
"name": "percent_minimum_similarity",
"type": "number",
"value": 70
}
]
}
},
"typeVersion": 3.4
},
{
"id": "ce7cbd23-b0ec-4420-a262-cc16ddd9f5bd",
"name": "Get articles from shopify",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1360,
496
],
"parameters": {
"url": "=https://{{ $('Workflow Configuration').first().json.shopifyStoreName }}.myshopify.com/admin/api/{{ $('Workflow Configuration').first().json.shopApiVersion }}/blogs/{{ $('Workflow Configuration').first().json.shopifyBlogId }}/articles.json",
"options": {},
"sendQuery": true,
"authentication": "predefinedCredentialType",
"queryParameters": {
"parameters": [
{
"name": "published_status",
"value": "published"
}
]
},
"nodeCredentialType": "shopifyAccessTokenApi"
},
"credentials": {
"shopifyAccessTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "3623adca-6827-467c-81f7-6f138377aefd",
"name": "Clean & Prepare Text",
"type": "n8n-nodes-base.code",
"position": [
-1136,
496
],
"parameters": {
"jsCode": "// Function to clean HTML and remove the related section\nfunction stripHtml(html) {\n if (!html) return '';\n return html\n .replace(/<style[^>]*>.*?<\\/style>/gi, '')\n .replace(/<script[^>]*>.*?<\\/script>/gi, '')\n .replace(/<div class=\"related-articles\"[^>]*>[\\s\\S]*?<\\/div>/gi, '') // remove related articles \n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .trim();\n}\n\n// Detect what we receive from previous node\nlet allItems = $input.all();\nlet articles = [];\n\n// 1: if multiple items\nif (allItems.length > 0) {\n allItems.forEach(item => {\n // Verificar si el item contiene un array \"articles\"\n if (item.json && Array.isArray(item.json.articles)) {\n articles = articles.concat(item.json.articles);\n } \n // if only one item\n else if (item.json && item.json.id && item.json.title) {\n articles.push(item.json);\n }\n });\n}\n\n// Fallback if no articles found\nif (articles.length === 0 && $input.item.json) {\n if (Array.isArray($input.item.json.articles)) {\n articles = $input.item.json.articles;\n } else if ($input.item.json.id && $input.item.json.title) {\n articles = [$input.item.json];\n }\n}\n\n\n// Process each article\nconst processedArticles = articles.map(article => {\n const cleanContent = stripHtml(article.body_html);\n const cleanSummary = stripHtml(article.summary_html);\n \n // Create text for embeddings \n const textForEmbedding = `T\u00edtulo: ${article.title}\n\nResumen: ${cleanSummary}\n\nContenido: ${cleanContent.substring(0, 15000)}`;\n\n return {\n shopify_id: article.id,\n blog_id: article.blog_id,\n shopify_handle: article.handle,\n title: article.title,\n content: cleanContent,\n summary: cleanSummary,\n url: $('Workflow Configuration').first().json.shopifyBlogDomain + article.handle,\n published_at: article.published_at,\n text_for_embedding: textForEmbedding,\n word_count: cleanContent.split(/\\s+/).filter(w => w.length > 0).length\n };\n});\n\nreturn processedArticles.map(article => ({ json: article }));"
},
"typeVersion": 2
},
{
"id": "a91f14ad-f42c-4ad9-9ddd-4a218e8b8514",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-912,
496
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "8a1ffb69-65c0-4cf5-a4cb-e9209a9dc0ea",
"name": "OpenAI Get Embeddings",
"type": "n8n-nodes-base.httpRequest",
"position": [
-464,
512
],
"parameters": {
"url": "https://api.openai.com/v1/embeddings",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"authentication": "predefinedCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "model",
"value": "text-embedding-3-small"
},
{
"name": "input",
"value": "={{ $json.embedding }}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"nodeCredentialType": "openAiApi"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "7d83e353-0272-4857-ad4e-f3baab2e62b6",
"name": "Set article fields",
"type": "n8n-nodes-base.set",
"position": [
-688,
512
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e443624a-de8d-485f-a3a1-b567c3145c2c",
"name": "shopify_id",
"type": "number",
"value": "={{ $json.shopify_id }}"
},
{
"id": "139eedb1-6aa3-434a-9425-fac9234c5148",
"name": "blog_id",
"type": "number",
"value": "={{ $json.blog_id }}"
},
{
"id": "9950f411-833a-488f-9f6a-b17f338114bf",
"name": "shopify_handle",
"type": "string",
"value": "={{ $json.shopify_handle }}"
},
{
"id": "1d83dc8d-c93d-41bb-ba5e-0a01f055c0fe",
"name": "title",
"type": "string",
"value": "={{ $json.title }}"
},
{
"id": "4a2a7109-b8d1-43f7-9d69-6d1f0cbef95a",
"name": "content",
"type": "string",
"value": "={{ $json.content }}"
},
{
"id": "0db25418-7777-4fc2-b774-4abf85819790",
"name": "summary",
"type": "string",
"value": "={{ $json.summary }}"
},
{
"id": "960a20c6-77ae-434a-9646-7b562da78a2e",
"name": "url",
"type": "string",
"value": "={{ $json.url }}"
},
{
"id": "4080709d-46e3-4d5c-b4b8-4cc599fae16c",
"name": "published_at",
"type": "string",
"value": "={{ $json.published_at }}"
},
{
"id": "83f98c7d-6ce7-4b58-9034-72f7b78a59e8",
"name": "embedding",
"type": "string",
"value": "={{ $json.text_for_embedding }})"
},
{
"id": "9a2a581c-2c3b-4a27-bde8-a3a62590a9df",
"name": "analyzed_at",
"type": "string",
"value": "={{new Date().toISOString()}}"
},
{
"id": "3a53791e-f7c6-4cef-ad55-8a37c9239073",
"name": "word_count",
"type": "number",
"value": "={{ $json.word_count }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "55ce71bc-afc1-4178-87a3-6fda9829bd68",
"name": "Calculate similarities",
"type": "n8n-nodes-base.code",
"position": [
-688,
48
],
"parameters": {
"jsCode": "const doneItems = $(\"Loop Over Items\").all();\n\nconsole.log(`\ud83d\udce6 Total items recibidos: ${doneItems.length}`);\n\n// Extract and prepare articles\nconst allArticles = doneItems.map(item => {\n const data = item.json;\n \n // Parsing embedding\n const embedding = typeof data.embedding === 'string' \n ? JSON.parse(data.embedding) \n : data.embedding;\n \n return {\n shopify_id: data.shopify_id,\n blog_id: data.blog_id,\n title: data.title,\n url: data.url,\n embedding: embedding\n };\n});\n\nconst uniqueIds = new Set(allArticles.map(a => a.shopify_id));\n\nif (allArticles.length < 2) {\n throw new Error(`\u274c Se necesitan al menos 2 art\u00edculos. Solo hay ${allArticles.length}`);\n}\n\nif (uniqueIds.size < 2) {\n throw new Error(`\u274c Todos los art\u00edculos tienen el mismo ID (${Array.from(uniqueIds)[0]})`);\n}\n\n// Function to calculate similarities\nfunction cosineSimilarity(vecA, vecB) {\n if (!vecA || !vecB || vecA.length !== vecB.length) {\n throw new Error('\u274c Embeddings inv\u00e1lidos o de diferente longitud');\n }\n \n let dotProduct = 0;\n let normA = 0;\n let normB = 0;\n \n for (let i = 0; i < vecA.length; i++) {\n dotProduct += vecA[i] * vecB[i];\n normA += vecA[i] * vecA[i];\n normB += vecB[i] * vecB[i];\n }\n \n const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));\n return Math.round(similarity * 100 * 100) / 100;\n}\n\nconst relations = [];\nlet comparacionesRealizadas = 0;\n\nfor (let i = 0; i < allArticles.length; i++) {\n for (let j = 0; j < allArticles.length; j++) {\n \n // \u26a0\ufe0f Rule to avoid comparing one article to itself\n if (allArticles[i].shopify_id === allArticles[j].shopify_id) {\n continue; // Saltar esta iteraci\u00f3n\n }\n \n comparacionesRealizadas++;\n \n try {\n const similarity = cosineSimilarity(\n allArticles[i].embedding,\n allArticles[j].embedding\n );\n \n // Only keep similarity above the setting\n if (similarity >= $('Workflow Configuration').first().json.percent_minimum_similarity) {\n relations.push({\n source_shopify_id: allArticles[i].shopify_id,\n source_blog_id: allArticles[i].blog_id,\n source_title: allArticles[i].title,\n related_shopify_id: allArticles[j].shopify_id,\n related_title: allArticles[j].title,\n related_url: allArticles[j].url,\n relation_type: \"semantic_similarity\",\n similarity_score: similarity\n });\n }\n } catch (error) {\n console.error(`Error comparando ${allArticles[i].shopify_id} con ${allArticles[j].shopify_id}:`, error.message);\n }\n }\n}\n\n// In case, not enough similarity\nif (relations.length === 0) {\n return [{ \n json: { \n mensaje: \"No se encontraron relaciones con similitud >= 60%\",\n articulos_procesados: allArticles.length,\n articulos_unicos: uniqueIds.size,\n comparaciones_realizadas: comparacionesRealizadas,\n sugerencia: \"Intenta bajar el umbral de similitud o verifica que los embeddings sean correctos\",\n muestra_articulos: allArticles.slice(0, 3).map(a => ({\n shopify_id: a.shopify_id,\n title: a.title,\n longitud_embedding: a.embedding.length\n }))\n } \n }];\n}\n\nreturn relations.map(rel => ({ json: rel }));"
},
"typeVersion": 2,
"alwaysOutputData": false
},
{
"id": "762e212e-1907-4610-9bcd-cc7b79258172",
"name": "Upsert articles",
"type": "n8n-nodes-base.dataTable",
"position": [
-240,
512
],
"parameters": {
"columns": {
"value": {
"url": "={{ $('Set article fields').item.json.url }}",
"title": "={{ $('Set article fields').item.json.title }}",
"blog_id": "={{ $('Set article fields').item.json.blog_id }}",
"content": "={{ $('Set article fields').item.json.content }}",
"summary": "={{ $('Set article fields').item.json.summary }}",
"embedding": "={{ JSON.stringify($json.data[0].embedding) }}",
"shopify_id": "={{ $('Set article fields').item.json.shopify_id }}",
"word_count": "={{ $('Set article fields').item.json.word_count }}",
"published_at": "={{ $('Set article fields').item.json.published_at }}",
"shopify_handle": "={{ $('Set article fields').item.json.shopify_handle }}",
"last_analyzed_at": "={{ $('Set article fields').item.json.analyzed_at }}"
},
"schema": [
{
"id": "shopify_id",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "shopify_id",
"defaultMatch": false
},
{
"id": "shopify_handle",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "shopify_handle",
"defaultMatch": false
},
{
"id": "title",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "title",
"defaultMatch": false
},
{
"id": "content",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "content",
"defaultMatch": false
},
{
"id": "summary",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "summary",
"defaultMatch": false
},
{
"id": "url",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "url",
"defaultMatch": false
},
{
"id": "published_at",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "published_at",
"defaultMatch": false
},
{
"id": "embedding",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "embedding",
"defaultMatch": false
},
{
"id": "last_analyzed_at",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "last_analyzed_at",
"defaultMatch": false
},
{
"id": "word_count",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "word_count",
"defaultMatch": false
},
{
"id": "blog_id",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "blog_id",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"filters": {
"conditions": [
{
"keyName": "shopify_id",
"keyValue": "={{ $('Set article fields').item.json.shopify_id }}"
}
]
},
"options": {},
"matchType": "allConditions",
"operation": "upsert",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "kjtDGPfteUD1V64N",
"cachedResultUrl": "/projects/XqoBcIeUrbWmIJt8/datatables/kjtDGPfteUD1V64N",
"cachedResultName": "articles"
}
},
"typeVersion": 1
},
{
"id": "6607766a-147e-49b6-b127-6655d3221c8c",
"name": "Upsert article_relations",
"type": "n8n-nodes-base.dataTable",
"position": [
-464,
48
],
"parameters": {
"columns": {
"value": {
"related_url": "={{ $json.related_url }}",
"source_title": "={{ $json.source_title }}",
"related_title": "={{ $json.related_title }}",
"source_blog_id": "={{ $json.source_blog_id }}",
"similarity_score": "={{ $json.similarity_score }}",
"source_shopify_id": "={{ $json.source_shopify_id }}",
"related_shopify_id": "={{ $json.related_shopify_id }}"
},
"schema": [
{
"id": "source_shopify_id",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "source_shopify_id",
"defaultMatch": false
},
{
"id": "source_blog_id",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "source_blog_id",
"defaultMatch": false
},
{
"id": "source_title",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "source_title",
"defaultMatch": false
},
{
"id": "related_shopify_id",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "related_shopify_id",
"defaultMatch": false
},
{
"id": "related_title",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "related_title",
"defaultMatch": false
},
{
"id": "related_url",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "related_url",
"defaultMatch": false
},
{
"id": "relation_type",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "relation_type",
"defaultMatch": false
},
{
"id": "similarity_score",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "similarity_score",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"filters": {
"conditions": [
{
"keyName": "source_shopify_id",
"keyValue": "={{ $json.source_shopify_id }}"
},
{
"keyName": "related_shopify_id",
"keyValue": "={{ $json.related_shopify_id }}"
}
]
},
"options": {},
"matchType": "allConditions",
"operation": "upsert",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "KXwn5vzq2gEz09tM",
"cachedResultUrl": "/projects/XqoBcIeUrbWmIJt8/datatables/KXwn5vzq2gEz09tM",
"cachedResultName": "article_relations"
}
},
"typeVersion": 1
},
{
"id": "ff514a42-ee01-448a-8951-4b6d82ecff01",
"name": "Top 3 related articles",
"type": "n8n-nodes-base.code",
"position": [
-240,
48
],
"parameters": {
"jsCode": "// YOUR_AWS_SECRET_KEY_HERE====\n// GROUP AND GET TOP 3 RELATED ARTICLES\n// YOUR_AWS_SECRET_KEY_HERE====\n\nconst relations = $(\"Calculate similarities\").all().map(item => item.json);\n\n// Group by source_shopify_id\nconst groupedBySource = relations.reduce((acc, relation) => {\n const sourceId = relation.source_shopify_id;\n \n if (!acc[sourceId]) {\n acc[sourceId] = {\n source_shopify_id: sourceId,\n source_blog_id: relation.source_blog_id,\n source_title: relation.source_title,\n source_url: relation.source_url,\n related_articles: []\n };\n }\n \n // Add related article with its similarity score\n acc[sourceId].related_articles.push({\n related_shopify_id: relation.related_shopify_id,\n related_title: relation.related_title,\n related_url: relation.related_url,\n similarity_score: relation.similarity_score\n });\n \n return acc;\n}, {});\n\n// Convert to array and get top 3 for each source\nconst result = Object.values(groupedBySource).map(group => {\n // Sort by similarity_score (highest first) and take top 3\n const top3Related = group.related_articles\n .sort((a, b) => b.similarity_score - a.similarity_score)\n .slice(0, 3)\n .map(article => article.related_shopify_id);\n \n return {\n source_shopify_id: group.source_shopify_id,\n related_shopify_ids: top3Related\n };\n});\n\n// Return as separate items for next node\nreturn result.map(item => ({ json: item }));"
},
"typeVersion": 2
},
{
"id": "088f6ac5-4c84-4c63-ba89-ab778c3ce547",
"name": "Loop for source articles",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-16,
48
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "28f1bf7c-db78-4f1f-8db3-b00474dd7284",
"name": "Get previous related_articles",
"type": "n8n-nodes-base.dataTable",
"position": [
208,
64
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "source_shopify_id",
"keyValue": "={{ $json.source_shopify_id }}"
}
]
},
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "7RrUjeEF2Zfcxpsn",
"cachedResultUrl": "/projects/XqoBcIeUrbWmIJt8/datatables/7RrUjeEF2Zfcxpsn",
"cachedResultName": "article_related_links_snapshot"
}
},
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "268e6a49-f852-4056-b9cf-8b912f5b19e1",
"name": "Check if article already has related",
"type": "n8n-nodes-base.if",
"position": [
432,
64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "6a466a42-552d-4be0-a443-e2907f660ba4",
"operator": {
"type": "number",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.source_shopify_id }}",
"rightValue": "={{ $('Loop for source articles').item.json.related_shopify_ids }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "b41317b7-4a8c-4043-94e8-89b6add1c772",
"name": "Upsert relations",
"type": "n8n-nodes-base.dataTable",
"position": [
688,
80
],
"parameters": {
"columns": {
"value": {
"source_shopify_id": "={{ $('Loop for source articles').item.json.source_shopify_id }}",
"related_shopify_ids": "={{ $('Loop for source articles').item.json.related_shopify_ids.join() }}"
},
"schema": [
{
"id": "source_shopify_id",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "source_shopify_id",
"defaultMatch": false
},
{
"id": "related_shopify_ids",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "related_shopify_ids",
"defaultMatch": false
},
{
"id": "related_urls",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "related_urls",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"filters": {
"conditions": [
{
"keyName": "source_shopify_id",
"keyValue": "={{ $('Loop for source articles').item.json.source_shopify_id }}"
}
]
},
"options": {},
"operation": "upsert",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "7RrUjeEF2Zfcxpsn",
"cachedResultUrl": "/projects/XqoBcIeUrbWmIJt8/datatables/7RrUjeEF2Zfcxpsn",
"cachedResultName": "article_related_links_snapshot"
}
},
"typeVersion": 1
},
{
"id": "2d051e8b-9f67-4ed3-8a2a-85d19281452f",
"name": "Prepare HTML for related articles",
"type": "n8n-nodes-base.code",
"position": [
1344,
80
],
"parameters": {
"jsCode": "// YOUR_AWS_SECRET_KEY_HERE====\n// PREPARE HTML FOR RELATED ARTICLES\n// YOUR_AWS_SECRET_KEY_HERE====\n\n// Get source article ID from \"Upsert relations\" node\nconst sourceArticle = $('Upsert relations').first().json;\nconst sourceShopifyId = sourceArticle.source_shopify_id;\n\n// Get related articles details from \"Get related article details\" node\nconst relatedArticles = $('Get related article details').all().map(i => i.json);\n\n// Build the HTML for related articles section\nlet relatedHTML = `\n<div class=\"related-articles\" style=\"margin: 40px 0;\">\n <h3>\ud83d\udcda Art\u00edculos relacionados que te pueden interesar</h3>\n <ul style=\"list-style: none; padding: 0;\">\n`;\n\n// Add each related article as a link\nrelatedArticles.forEach(article => {\n relatedHTML += `\n <li style=\"margin-bottom: 10px;\">\n <a href=\"${article.url}\">\u2192 ${article.title}</a>\n </li>\n `;\n});\n\nrelatedHTML += `\n </ul>\n</div>\n`;\n\nreturn {\n json: {\n source_shopify_id: sourceShopifyId,\n related_articles_html: relatedHTML,\n related_count: relatedArticles.length\n }\n};"
},
"typeVersion": 2
},
{
"id": "0e6e4ad1-cf86-4e35-a862-bc8219c99838",
"name": "Check if related articles are the same",
"type": "n8n-nodes-base.if",
"position": [
608,
-288
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "320c269c-572b-4888-b6c0-7ceec4db0e30",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $('Loop for source articles').item.json.related_shopify_ids.join() }}",
"rightValue": "={{ $('Get previous related_articles').item.json.related_shopify_ids }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "73604394-344d-469e-b845-0f3660abccaf",
"name": "No need to change the related section",
"type": "n8n-nodes-base.noOp",
"position": [
816,
-304
],
"parameters": {},
"typeVersion": 1
},
{
"id": "2bc885ba-cbc4-4623-82ac-e85759170a6d",
"name": "Get related article details",
"type": "n8n-nodes-base.dataTable",
"position": [
1120,
80
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "shopify_id",
"keyValue": "={{ $json.related_shopify_id }}"
}
]
},
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "kjtDGPfteUD1V64N",
"cachedResultUrl": "/projects/XqoBcIeUrbWmIJt8/datatables/kjtDGPfteUD1V64N",
"cachedResultName": "articles"
}
},
"typeVersion": 1
},
{
"id": "85357d92-5b8b-4e36-acba-a1f3473237ae",
"name": "Split Related IDs",
"type": "n8n-nodes-base.code",
"position": [
896,
80
],
"parameters": {
"jsCode": "return $input.all().flatMap(item => {\n const ids = item.json.related_shopify_ids.split(',').map(id => id.trim());\n return ids.map(relatedId => ({\n json: {\n source_shopify_id: item.json.source_shopify_id,\n related_shopify_id: relatedId\n }\n }));\n});"
},
"typeVersion": 2
},
{
"id": "36042f8c-f7b5-48bc-949f-44f8521d407e",
"name": "Insert Related in Article",
"type": "n8n-nodes-base.code",
"position": [
1568,
80
],
"parameters": {
"jsCode": "// YOUR_AWS_SECRET_KEY_HERE====\n// UPDATE SHOPIFY ARTICLE WITH RELATED LINKS\n// YOUR_AWS_SECRET_KEY_HERE====\n\nconst item = $input.item.json;\n\n// Get current article content from Shopify\nconst currentArticle = $('Get articles from shopify').first().json.articles.find(a => a.id === $input.first().json.source_shopify_id);\nlet currentContent = currentArticle.body_html || '';\n\n// Remove old related articles section if exists\ncurrentContent = currentContent.replace(\n /<div class=\"related-articles\"[^>]*>[\\s\\S]*?<\\/div>/gi, \n ''\n);\n\n// Add new related articles at the end\nconst updatedContent = currentContent.trim() + '\\n\\n' + item.related_articles_html;\n\nconst article = {\n id: item.source_shopify_id,\n body_html: updatedContent\n}\n\nreturn [{ json: { article } }];"
},
"typeVersion": 2
},
{
"id": "1524f17c-e0b2-41dc-a948-4e085885b0a2",
"name": "Update article with related",
"type": "n8n-nodes-base.httpRequest",
"position": [
1792,
80
],
"parameters": {
"url": "=https://{{ $('Workflow Configuration').first().json.shopifyStoreName }}.myshopify.com/admin/api/{{ $('Workflow Configuration').first().json.shopApiVersion }}/blogs/{{ $('Workflow Configuration').first().json.shopifyBlogId }}/articles/{{ $json.article.id }}.json",
"method": "PUT",
"options": {},
"jsonBody": "={{ JSON.stringify($json) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "shopifyAccessTokenApi"
},
"credentials": {
"shopifyAccessTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "e9d3c731-ff01-4bbc-9c8c-a6eed48abb20",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-976,
304
],
"parameters": {
"width": 896,
"height": 480,
"content": "## Analyze each article with \"text-embedding-3-small\" model and save to Data Table\n* Previously it cleaned the content for each article\n* Within this loop for each article it's generating the embedding that will then be used to analyze similarities\n* It saves each article in the \"articles\" Data Table"
},
"typeVersion": 1
},
{
"id": "d61c23f1-2de2-4b63-9e3b-a38cbb37688d",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-768,
-448
],
"parameters": {
"color": 5,
"width": 2736,
"height": 736,
"content": "## Core process\n* It compares each article to each other to calculate the semantic similarity \n* In that particular example it looks for article that have more than 70% semantic similarity. This can be changed in the \"Workflow Configuration\" node, the variable is called \"percent_minimum_similarity\" \n* For each article, it gets the top 3 related articles ordered by highest semantic similarity. If you want to include more related articles you can change the node \"Top 3 related articles\"\n* It then checks in the \"article_related_links_snapshot\" Data table to check if this article already had related articles. Then it checks if the related are the same or need to be updated in the article\n* The rest of the process is to get the details of each related articles, create the html section, replace it within the article if there is already one and finally update the article through shopify API"
},
"typeVersion": 1
},
{
"id": "4d66e34f-f02c-4b30-8a7e-182b828330fd",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1856,
304
],
"parameters": {
"color": 4,
"width": 864,
"height": 480,
"content": "## Set up\n* **Workflow Configuration node need to be updated with your info**"
},
"typeVersion": 1
},
{
"id": "03a381af-5bf9-4fa1-a633-3e2a154b1ceb",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1856,
-448
],
"parameters": {
"color": 7,
"width": 1072,
"height": 736,
"content": "## IMPORTANT READ BEFORE LAUNCHING THE WORKFLOW\nFor this workflow to work you first need to create three Data Tables. Below is the details of each one. \n**articles**\n** shopify_id (number)\n** shopify_handle (string)\n** title (string)\n** content (string) \n** summary (string)\n** url (string)\n** published_at (datetime)\n** embedding (string)\n** last_analyzed_at (datetime)\n** word_count (number) \n** blog_id (number) \n\n**article_relations**\n** source_shopify_id (number) \n** source_blog_id (number) \n** source_title (string)\n** related_shopify_id (number) \n** related_title (string)\n** related_url (string) \n** similarity_score (number)\n\n**article_related_links_snapshot**\n** source_shopify_id (number)\n** related_shopify_ids (string) \n\n\nThen, the only thing you need to configure is the second node \"Workflow Configuration\". "
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": "ib0LqomNsNIqEoi6",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "f2d0a7cc-1be7-4878-bd20-e82dae800398",
"connections": {
"Loop Over Items": {
"main": [
[
{
"node": "Calculate similarities",
"type": "main",
"index": 0
}
],
[
{
"node": "Set article fields",
"type": "main",
"index": 0
}
]
]
},
"Upsert articles": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Workflow Configuration",
"type": "main",
"index": 0
}
]
]
},
"Upsert relations": {
"main": [
[
{
"node": "Split Related IDs",
"type": "main",
"index": 0
}
]
]
},
"Split Related IDs": {
"main": [
[
{
"node": "Get related article details",
"type": "main",
"index": 0
}
]
]
},
"Set article fields": {
"main": [
[
{
"node": "OpenAI Get Embeddings",
"type": "main",
"index": 0
}
]
]
},
"Clean & Prepare Text": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Get Embeddings": {
"main": [
[
{
"node": "Upsert articles",
"type": "main",
"index": 0
}
]
]
},
"Calculate similarities": {
"main": [
[
{
"node": "Upsert article_relations",
"type": "main",
"index": 0
}
]
]
},
"Top 3 related articles": {
"main": [
[
{
"node": "Loop for source articles",
"type": "main",
"index": 0
}
]
]
},
"Workflow Configuration": {
"main": [
[
{
"node": "Get articles from shopify",
"type": "main",
"index": 0
}
]
]
},
"Loop for source articles": {
"main": [
[],
[
{
"node": "Get previous related_articles",
"type": "main",
"index": 0
}
]
]
},
"Upsert article_relations": {
"main": [
[
{
"node": "Top 3 related articles",
"type": "main",
"index": 0
}
]
]
},
"Get articles from shopify": {
"main": [
[
{
"node": "Clean & Prepare Text",
"type": "main",
"index": 0
}
]
]
},
"Insert Related in Article": {
"main": [
[
{
"node": "Update article with related",
"type": "main",
"index": 0
}
]
]
},
"Get related article details": {
"main": [
[
{
"node": "Prepare HTML for related articles",
"type": "main",
"index": 0
}
]
]
},
"Update article with related": {
"main": [
[
{
"node": "Loop for source articles",
"type": "main",
"index": 0
}
]
]
},
"Get previous related_articles": {
"main": [
[
{
"node": "Check if article already has related",
"type": "main",
"index": 0
}
]
]
},
"Prepare HTML for related articles": {
"main": [
[
{
"node": "Insert Related in Article",
"type": "main",
"index": 0
}
]
]
},
"Check if article already has related": {
"main": [
[
{
"node": "Check if related articles are the same",
"type": "main",
"index": 0
}
],
[
{
"node": "Upsert relations",
"type": "main",
"index": 0
}
]
]
},
"No need to change the related section": {
"main": [
[
{
"node": "Loop for source articles",
"type": "main",
"index": 0
}
]
]
},
"Check if related articles are the same": {
"main": [
[
{
"node": "No need to change the related section",
"type": "main",
"index": 0
}
],
[
{
"node": "Upsert relations",
"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.
openAiApishopifyAccessTokenApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Automatically surface and insert the three most relevant “Related articles” at the end of every Shopify blog post to boost session depth, SEO, and reader engagement.
Source: https://n8n.io/workflows/10542/ — 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.
Who is this for? Event organizers losing 80% of form starters who never finish registration and want automated follow-up emails triggered by abandonment beacons. What problem is this workflow solving?
> Set up n8n self-hosted via Tino.vn VPS — use code VPSN8N for up to 39% off (affiliate link).
Automatically publish Lightroom photos to Instagram with short, human-sounding AI captions. This workflow pulls the next item from your Data Table queue, generates an on-brand caption from alt text +
This template is perfect for TikTok creators, content marketers, and social media teams who want to turn viral comments into engaging short-form videos without manually scripting, recording, or editin
Create a reusable “photos to post” queue from your Lightroom Cloud album—ideal for Lightroom-to-Instagram automation with n8n. It discovers new photos, stores clean metadata in a Data Table, and gener