This workflow corresponds to n8n.io template #17146 — we link there as the canonical source.
This workflow follows the Agent → Documentdefaultdataloader 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 →
{
"meta": {
"templateCredsSetupCompleted": false
},
"name": "DigitalCube - Digital Clone of the Internal KB (Notion + Pinecone + Slack)",
"nodes": [
{
"id": "78ee873a-cbd7-4c67-9fe9-9457f6bafbd7",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1008,
1504
],
"parameters": {
"width": 480,
"height": 896,
"content": "## DigitalCube - Digital Clone of the Internal KB (Notion + Pinecone + Slack)\n\n### How it works\n\nThis workflow keeps an internal knowledge base in Pinecone synchronized from Notion and exposes it as a Slack-based assistant. A daily sync pulls Notion KB pages and blocks, prepares documents with metadata, chunks and embeds them, then upserts vectors into Pinecone. When a user posts in the configured Slack help channel, the workflow filters out bot/system messages, asks CubeBot to search the Pinecone KB with OpenAI-powered retrieval, and replies in the same Slack thread citing the exact source Notion page.\n\n### Setup steps\n\n- Create a Notion internal integration at notion.so/my-integrations, connect its token as credentials in n8n, and share your Knowledge Base database with the integration (\u2022\u2022\u2022 \u2192 Connections in Notion).\n- Create a Pinecone index (dimension 1536, metric cosine, matching text-embedding-3-small), then add Pinecone credentials to both the ingestion and query vector store nodes. The namespace is created automatically on the first run.\n- Add OpenAI credentials for the chat model and both ingestion/query embedding nodes. Both embedding nodes MUST use the same model.\n- Create a Slack app with the channels:history and chat:write scopes, enable Event Subscriptions pointing to this workflow's production webhook URL (activate the workflow first), subscribe to message.channels, invite the bot to your help channel and paste the channel ID (C0...) in the Slack trigger. Use the Bot token (xoxb-...), never the user token (xoxp-...).\n- Review the daily schedule, the bot/system-message filter conditions, and the CubeBot agent prompt/tool settings, run the ingestion branch once manually, then activate the workflow.\n\n### Customization\n\nAdjust chunk size/overlap, document metadata fields, Pinecone namespace, Slack channel, and the AI agent instructions to match the structure and tone of your internal KB. You can swap Pinecone for Qdrant or Supabase/pgvector by replacing the two vector store nodes, and add Google Drive or PDF sources by duplicating the ingestion branch into the same index."
},
"typeVersion": 1
},
{
"id": "85d4a3e3-220d-4074-a99e-e5e0d9d98d58",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1568,
1536
],
"parameters": {
"color": 7,
"width": 624,
"height": 304,
"content": "## Fetch Notion content\n\nStarts the daily synchronization and retrieves the relevant Notion KB pages and their block content from the configured workspace."
},
"typeVersion": 1
},
{
"id": "7672c4d8-a074-4f8c-8e68-ce5b341c4401",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
2224,
1504
],
"parameters": {
"color": 7,
"width": 608,
"height": 752,
"content": "## Prepare and index documents\n\nTransforms Notion content into metadata-rich documents, loads and splits the text into chunks, creates ingestion embeddings, and inserts the resulting vectors into Pinecone."
},
"typeVersion": 1
},
{
"id": "364bc3bd-8c5d-4f9b-aa61-2ee5bf3fa40c",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1632,
2448
],
"parameters": {
"color": 7,
"width": 512,
"height": 320,
"content": "## Receive Slack questions\n\nListens for messages in the help channel and filters out bot or system-generated events before passing valid user questions onward."
},
"typeVersion": 1
},
{
"id": "e9d141e5-677f-4394-9f62-e39c252b0d52",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2176,
2432
],
"parameters": {
"color": 7,
"width": 464,
"height": 752,
"content": "## Generate KB answer\n\nRuns CubeBot with an OpenAI chat model and a Pinecone knowledge-base search tool, using query embeddings to retrieve relevant context before composing an answer."
},
"typeVersion": 1
},
{
"id": "7cc1faf1-e46d-463a-9ee9-8646164f7472",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
2672,
2400
],
"parameters": {
"color": 7,
"width": 240,
"height": 368,
"content": "## Post Slack reply\n\nSends CubeBot's final response back to Slack as a threaded reply, keeping the answer attached to the original user message."
},
"typeVersion": 1
},
{
"id": "a1c0f0d1-091d-4361-9b89-0d1977c9bdf6",
"name": "Split Text into Chunks",
"type": "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter",
"notes": "1,000 characters with a 200-character overlap. We tried 500, but the bot lost context on long onboarding manuals; with 2,000, it added too much filler to the answers.",
"position": [
2688,
2096
],
"parameters": {
"options": {},
"chunkSize": 1000,
"chunkOverlap": 200
},
"typeVersion": 1
},
{
"id": "a81c9c24-4522-4728-b7d2-a8da261ad4bc",
"name": "CubeBot AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"notes": "The brain of CubeBot. The system prompt was polished between Automation and People over two weeks: Rule 4 (admitting when it doesn't know) was an express request from Legal. If you change the prompt, test it in a staging channel first before touching production.",
"position": [
2336,
2608
],
"parameters": {
"text": "={{ $('Slack KB Channel Trigger').item.json.text }}",
"options": {
"systemMessage": "You are CubeBot, the assistant for the company's internal Knowledge Base. Your sole source of truth is the `knowledge_base` tool, which searches through vectorized internal documentation (Notion: onboarding manuals, processes, policies, guidelines).\n\nRules:\n\n1. ALWAYS use the `knowledge_base` tool before answering. Never answer from memory.\n2. Answer in the same language as the question, clearly and concisely, using a friendly tone.\n3. ALWAYS CITE the exact source: each result from the tool includes `source_title` and `source_url` metadata. End your response with a `\ud83d\udcc4 Sources:` section listing each document used as a Slack link with the format <URL|Title>.\n4. If the information does not appear in the knowledge base, say so honestly: \"I couldn't find this in the internal documentation\" and suggest asking the relevant team or manager. DO NOT make up answers.\n5. Format for Slack: use *bold*, bulleted lists, and code blocks when applicable. Do not use Markdown headers (#).\n6. If the question is ambiguous, answer as best as possible with what you found and indicate what details would help refine the search."
},
"promptType": "define"
},
"typeVersion": 1.9
},
{
"id": "66ee385e-a07a-408f-9fdc-0912c9b12939",
"name": "OpenAI GPT-4 Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"notes": "GPT-4o with temperature 0.2: we want factual answers, not creativity.",
"position": [
2320,
2832
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o"
},
"options": {
"temperature": 0.2
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "fdcdac0b-3397-4480-a4e0-2380eba045b9",
"name": "Daily Sync Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "Set for 2:00 AM because that's when no one is asking questions on Slack and the content team has already finished editing Notion. If the KB starts changing faster (e.g., during massive onboardings), lower it to every 6 hours, keeping embedding costs in mind.",
"position": [
1616,
1664
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 24,
"triggerAtHour": 2
}
]
}
},
"typeVersion": 1.2
},
{
"id": "d3410441-15ba-4009-b61a-2d9f50616ef9",
"name": "Fetch Notion KB Pages",
"type": "n8n-nodes-base.notion",
"notes": "Point it to your master KB database in Notion. REMINDER for anyone cloning this: the Notion integration DOES NOT see pages by default; you have to share the database with the integration from 'Connections' in Notion.",
"position": [
1840,
1664
],
"parameters": {
"options": {},
"resource": "databasePage",
"operation": "getAll",
"returnAll": true,
"databaseId": {
"__rl": true,
"mode": "list",
"value": "REPLACE",
"cachedResultUrl": "",
"cachedResultName": ""
}
},
"credentials": {
"notionApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "43239629-d52b-44b5-bac3-d50e9f2aa6d2",
"name": "Retrieve Notion Blocks",
"type": "n8n-nodes-base.notion",
"notes": "It pulls the actual content of each page, including nested blocks (toggles, lists within lists... you know how Product teams document). With 'fetchNestedBlocks' enabled, nothing gets lost, but if the KB grows too large, this node will become the bottleneck: keep an eye on it during execution runs.",
"position": [
2048,
1664
],
"parameters": {
"blockId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.id }}"
},
"resource": "block",
"operation": "getAll",
"returnAll": true,
"fetchNestedBlocks": true
},
"credentials": {
"notionApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "3eb829e2-7964-46e6-a511-6e071194742d",
"name": "Prepare Document Metadata",
"type": "n8n-nodes-base.code",
"notes": "The heart of the ingestion process. It merges the blocks of each page into a single document and appends the metadata (title, Notion URL, date). If the bot ever cites 'Untitled', the issue is right here: the title property name changed in Notion and needs to be added to the fallback.",
"position": [
2272,
1664
],
"parameters": {
"jsCode": "// ============================================================\n// DigitalCube \u00b7 Document preparation for the vector KB\n// Author: Internal Automation Team\n// ------------------------------------------------------------\n// Groups blocks by page and builds one document per page\n// with its full text + metadata. Metadata is SACRED:\n// without source_url, the bot cannot cite the source,\n// making the entire project pointless.\n// ============================================================\n\nconst pages = $('Fetch Notion KB Pages').all();\nconst blocks = $input.all();\n\n// Index of pages by ID to retrieve title and URL\nconst pageInfo = {};\nfor (const p of pages) {\n const pj = p.json;\n const title =\n pj.name ||\n pj.title ||\n (pj.properties?.Name?.title?.[0]?.plain_text) ||\n (pj.properties?.title?.title?.[0]?.plain_text) ||\n 'Untitled';\n pageInfo[pj.id] = {\n title,\n url: pj.url || `https://www.notion.so/${String(pj.id).replace(/-/g, '')}`,\n lastEdited: pj.last_edited_time || null,\n };\n}\n\n// Extracts plain text from a block (raw or simplified n8n format)\nfunction blockText(b) {\n if (typeof b.content === 'string') return b.content;\n const type = b.type;\n const rt = b[type]?.rich_text || b[type]?.text || [];\n if (Array.isArray(rt)) {\n return rt.map((t) => t.plain_text || t.text?.content || '').join('');\n }\n return '';\n}\n\n// Which page each block belongs to (varies by Notion node version)\nfunction blockPageId(b) {\n return b.root_id || b.parent?.page_id || b.parent_id || null;\n}\n\nconst byPage = {};\nfor (const item of blocks) {\n const b = item.json;\n const pid = blockPageId(b) || Object.keys(pageInfo)[0];\n const text = blockText(b);\n if (!text || !text.trim()) continue;\n if (!byPage[pid]) byPage[pid] = [];\n byPage[pid].push(text.trim());\n}\n\nconst output = [];\nfor (const [pid, texts] of Object.entries(byPage)) {\n const info = pageInfo[pid] || { title: 'Internal Document', url: '', lastEdited: null };\n output.push({\n json: {\n page_content: `# ${info.title}\\n\\n${texts.join('\\n')}`,\n source_title: info.title,\n source_url: info.url,\n last_edited: info.lastEdited,\n },\n });\n}\n\nreturn output;"
},
"typeVersion": 2
},
{
"id": "176c6f92-94bf-438f-a135-35fc7ac1dd99",
"name": "Insert Vectors into Pinecone",
"type": "@n8n/n8n-nodes-langchain.vectorStorePinecone",
"notes": "We insert everything into the 'internal-kb' namespace. The 'clearNamespace: true' is on purpose: we prefer to rebuild the index every night rather than drag along chunks from documents we've already deleted. If you want incremental sync, treat it as a deliberate change, not a quick edit.",
"position": [
2512,
1664
],
"parameters": {
"mode": "insert",
"options": {
"clearNamespace": true,
"pineconeNamespace": "internal-kb"
},
"pineconeIndex": {
"__rl": true,
"mode": "list",
"value": "knowledge-base-interna",
"cachedResultName": "knowledge-base-interna"
}
},
"credentials": {
"pineconeApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "b8191570-f9fa-4a13-8f94-c22ab7b91695",
"name": "OpenAI Embeddings Ingestion",
"type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
"notes": "We use text-embedding-3-small: it's more than enough for a typical KB size and dirt cheap. WARNING: the Pinecone index is created with 1536 dimensions; if anyone changes the embedding model without recreating the index, the ingestion will blow up.",
"position": [
2432,
1888
],
"parameters": {
"model": "text-embedding-3-small",
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "7abf850d-a0c1-455c-a74f-f55589072914",
"name": "Load Data for Vectors",
"type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader",
"notes": "This is where each chunk inherits source_title and source_url. This is what sets the bot apart from a generic ChatGPT: when someone asks about the vacation process, the bot links to THE exact Notion page. Do not touch this metadata without updating the agent's system prompt as well.",
"position": [
2608,
1888
],
"parameters": {
"options": {
"metadata": {
"metadataValues": [
{
"name": "source_title",
"value": "={{ $json.source_title }}"
},
{
"name": "source_url",
"value": "={{ $json.source_url }}"
},
{
"name": "last_edited",
"value": "={{ $json.last_edited }}"
}
]
}
},
"jsonData": "={{ $json.page_content }}",
"jsonMode": "expressionData"
},
"typeVersion": 1
},
{
"id": "c6448e7d-1cd2-413f-89d5-48aac6ed7f94",
"name": "Slack KB Channel Trigger",
"type": "n8n-nodes-base.slackTrigger",
"notes": "It only listens to a single help channel (ours is #ayuda-kb), which was intentionally decided with People: if we open it to all channels, the bot will butt into conversations where nobody asked for it. The Slack App needs Event Subscriptions pointing to the n8n webhook and the channels:history + chat:write scopes. The channel ID is copied from Slack: right-click on the channel \u2192 'Copy link' (the ID is the part starting with C).",
"position": [
1680,
2608
],
"parameters": {
"options": {},
"trigger": [
"message"
],
"channelId": {
"__rl": true,
"mode": "id",
"value": "REPLACE_WITH_CHANNEL_ID"
}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "5a5c17bd-c067-4d5d-9384-6ea740a74096",
"name": "Filter Out Bots and System Messages",
"type": "n8n-nodes-base.if",
"notes": "THE MOST IMPORTANT NODE OF THE WORKFLOW, seriously. It filters out bot messages (including CubeBot itself) and system subtypes. Without this, the bot reads its own response, replies to itself, and enters an infinite loop. Do not delete it 'to simplify things.' Related: use the Bot token (xoxb-...) in the Slack credentials, never the user token (xoxp-...), or replies may not carry a bot_id and this filter loses its effect.",
"position": [
2000,
2608
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "cond-no-bot",
"operator": {
"type": "string",
"operation": "empty",
"singleValue": true
},
"leftValue": "={{ $json.bot_id }}",
"rightValue": ""
},
{
"id": "cond-no-subtype",
"operator": {
"type": "string",
"operation": "empty",
"singleValue": true
},
"leftValue": "={{ $json.subtype }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "7c1cf35a-3b81-4d06-ae24-22a7c0808261",
"name": "Pinecone KB Search",
"type": "@n8n/n8n-nodes-langchain.vectorStorePinecone",
"notes": "The vector store exposed as an agent tool. topK=5 was the sweet spot in our testing: with 3, it missed things; with 10, the context got cluttered with noise. 'includeDocumentMetadata' HAS to be enabled, otherwise CubeBot won't be able to cite sources.",
"position": [
2512,
2832
],
"parameters": {
"mode": "retrieve-as-tool",
"topK": 5,
"options": {
"pineconeNamespace": "internal-kb"
},
"toolName": "knowledge_base",
"pineconeIndex": {
"__rl": true,
"mode": "list",
"value": "knowledge-base-interna",
"cachedResultName": "knowledge-base-interna"
},
"toolDescription": "Search the internal company documentation (Notion: onboarding, processes, policies, manuals). ALWAYS use it to answer any employee question. Return relevant snippets with source_title and source_url metadata to cite the source.",
"includeDocumentMetadata": true
},
"credentials": {
"pineconeApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "7e97a63e-8913-44e8-86b3-ce27014e024b",
"name": "OpenAI Embeddings Query",
"type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
"notes": "The EXACT SAME model as in ingestion (text-embedding-3-small), absolutely mandatory. If ingestion and query use different embeddings, semantic search silently returns garbage: it won't throw an error, just bad results. It's the sneakiest bug in the entire setup.",
"position": [
2496,
3024
],
"parameters": {
"model": "text-embedding-3-small",
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "9e18bb43-c99d-4b63-95b6-7ef258e54b71",
"name": "Post Reply in Slack Thread",
"type": "n8n-nodes-base.slack",
"notes": "Always reply in a thread, never in the main channel. This was requested by the entire team after the first week: the help channel became unreadable with unthreaded replies. The 'thread_ts' points to the ts of the original trigger message.",
"position": [
2720,
2608
],
"parameters": {
"text": "={{ $json.output }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Slack KB Channel Trigger').item.json.channel }}"
},
"otherOptions": {
"mrkdwn": true,
"thread_ts": {
"replyValues": {
"thread_ts": "={{ $('Slack KB Channel Trigger').item.json.ts }}"
}
}
}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"connections": {
"CubeBot AI Agent": {
"main": [
[
{
"node": "Post Reply in Slack Thread",
"type": "main",
"index": 0
}
]
]
},
"Daily Sync Trigger": {
"main": [
[
{
"node": "Fetch Notion KB Pages",
"type": "main",
"index": 0
}
]
]
},
"Pinecone KB Search": {
"ai_tool": [
[
{
"node": "CubeBot AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Fetch Notion KB Pages": {
"main": [
[
{
"node": "Retrieve Notion Blocks",
"type": "main",
"index": 0
}
]
]
},
"Load Data for Vectors": {
"ai_document": [
[
{
"node": "Insert Vectors into Pinecone",
"type": "ai_document",
"index": 0
}
]
]
},
"Retrieve Notion Blocks": {
"main": [
[
{
"node": "Prepare Document Metadata",
"type": "main",
"index": 0
}
]
]
},
"Split Text into Chunks": {
"ai_textSplitter": [
[
{
"node": "Load Data for Vectors",
"type": "ai_textSplitter",
"index": 0
}
]
]
},
"OpenAI Embeddings Query": {
"ai_embedding": [
[
{
"node": "Pinecone KB Search",
"type": "ai_embedding",
"index": 0
}
]
]
},
"OpenAI GPT-4 Chat Model": {
"ai_languageModel": [
[
{
"node": "CubeBot AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Slack KB Channel Trigger": {
"main": [
[
{
"node": "Filter Out Bots and System Messages",
"type": "main",
"index": 0
}
]
]
},
"Prepare Document Metadata": {
"main": [
[
{
"node": "Insert Vectors into Pinecone",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Embeddings Ingestion": {
"ai_embedding": [
[
{
"node": "Insert Vectors into Pinecone",
"type": "ai_embedding",
"index": 0
}
]
]
},
"Filter Out Bots and System Messages": {
"main": [
[
{
"node": "CubeBot AI Agent",
"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.
notionApiopenAiApipineconeApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow syncs an internal Notion knowledge base to Pinecone on a daily schedule, then runs a Slack bot that answers questions in a specific channel using Pinecone retrieval and an OpenAI chat model, replying in a thread with citations back to the source Notion pages. Runs…
Source: https://n8n.io/workflows/17146/ — 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.
This workflow automates patient communication for medical clinics using the WhatsApp Business API. It supports appointment booking, rescheduling, service inquiries, follow-ups, and document submission
WooriFisa 최종. Uses memoryMongoDbChat, agent, httpRequest, documentDefaultDataLoader. Scheduled trigger; 68 nodes.
This workflow automates end-to-end e-commerce order processing from intake through fulfillment by orchestrating multiple AI-powered validation stages and external system integrations. Designed for e-c
Every day at 8 AM, the workflow automatically retrieves the latest F1 data—including driver standings, qualifying results, race schedules, and circuit information. All sources are merged into a unifie
Scheduled triggers initiate automated contract reviews. The system fetches documents from cloud storage and email, then uses AI to extract key terms, obligations, and compliance requirements. Multi-mo