This workflow corresponds to n8n.io template #16231 — we link there as the canonical source.
This workflow follows the Agent → 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": "kMRyUak645Y5w0iM",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Build a knowledge graph from Slack messages and store in Neo4j",
"tags": [],
"nodes": [
{
"id": "c4a21db4-27d0-419e-a95b-d3c52eb208a2",
"name": "Sticky Note - Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-16,
208
],
"parameters": {
"width": 980,
"height": 960,
"content": "## Build a knowledge graph from Slack messages and store in Neo4j\n\nThis workflow ingests internal company chat data (Slack, Teams, or webhook), extracts entities (people, decisions, topics, discussions), maps their relationships, structures everything as a knowledge graph, and persists it to a graph database (Neo4j) and a Google Sheet audit log.\n\n### Who's it for\n\u2022 Knowledge management teams\n\u2022 Engineering orgs wanting decision provenance\n\u2022 HR / People teams tracking cross-team collaboration\n\u2022 Product teams auditing discussion-to-decision lineage\n\n### How it works\n1. Ingests chat messages via webhook or scheduled Slack poll\n2. Normalises raw message payload\n3. Python pre-filter: skips noise (greetings, reactions, bot messages)\n4. AI entity extraction: people, topics, decisions, action items\n5. AI relationship mapper: who \u2194 what \u2194 when \u2194 outcome\n6. Graph structurer: converts to nodes + edges JSON schema\n7. Validates graph integrity (JS)\n8. Writes nodes/edges to Neo4j via HTTP API\n9. Appends audit row to Google Sheet\n10. Returns 200 confirmation via webhook response\n\n### Setup\n1. Import this workflow into n8n\n2. Configure credentials: Slack, Google Sheets, Neo4j, Anthropic\n3. Replace placeholder IDs (Sheet ID, Neo4j URL)\n4. Activate\n\n### Requirements\n\u2022 Slack API token (or Teams webhook)\n\u2022 Neo4j Aura / self-hosted instance\n\u2022 Google Sheets\n\u2022 Anthropic Claude API key\n\n### Customisation\n\u2022 Swap Neo4j nodes for ArangoDB or any graph DB\n\u2022 Adjust AI prompts to extract domain-specific entities\n\u2022 Add a second AI pass for sentiment / urgency scoring\n\u2022 Connect output to a BI dashboard (Metabase, Grafana)"
},
"typeVersion": 1
},
{
"id": "9e5305bd-7fc7-4fa8-acc4-7488b8d390cb",
"name": "Sticky Note - Ingest",
"type": "n8n-nodes-base.stickyNote",
"position": [
1072,
448
],
"parameters": {
"color": 6,
"width": 780,
"height": 520,
"content": "## 1. Ingest & Normalise"
},
"typeVersion": 1
},
{
"id": "3c4a26f2-a5e5-4bdc-9b86-c416b7886e17",
"name": "Sticky Note - AI",
"type": "n8n-nodes-base.stickyNote",
"position": [
1904,
368
],
"parameters": {
"color": 6,
"width": 840,
"height": 1140,
"content": "## 2. Entity Extraction & Relationship Mapping"
},
"typeVersion": 1
},
{
"id": "f76da905-9980-42aa-ab7e-1e938ed70d79",
"name": "Sticky Note - Graph",
"type": "n8n-nodes-base.stickyNote",
"position": [
2816,
416
],
"parameters": {
"color": 6,
"width": 768,
"height": 580,
"content": "## 3. Graph Structuring & Validation"
},
"typeVersion": 1
},
{
"id": "75388612-14b7-4e7e-8b80-3d51994b3247",
"name": "Sticky Note - Persist",
"type": "n8n-nodes-base.stickyNote",
"position": [
3664,
432
],
"parameters": {
"color": 6,
"width": 908,
"height": 576,
"content": "## 4. Persist & Audit"
},
"typeVersion": 1
},
{
"id": "1452cf91-57e0-4ac7-8443-9e78ef4919e5",
"name": "Webhook - Chat Message Ingest",
"type": "n8n-nodes-base.webhook",
"position": [
1232,
608
],
"parameters": {
"path": "chat-ingest",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 1.1
},
{
"id": "74ec6104-f069-46ac-8ee4-f4d38842fc41",
"name": "Schedule - Poll Slack Every 15 min",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
1232,
816
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "*/15 * * * *"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "3b2e3e92-79af-4a27-a395-646784428187",
"name": "Normalise Message Payload",
"type": "n8n-nodes-base.set",
"position": [
1504,
720
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "channelId",
"type": "string",
"value": "={{ $json.channel_id || $json.body?.channel_id || 'unknown-channel' }}"
},
{
"name": "channelName",
"type": "string",
"value": "={{ $json.channel_name || $json.body?.channel_name || 'general' }}"
},
{
"name": "senderId",
"type": "string",
"value": "={{ $json.user_id || $json.body?.user_id || $json.from || 'unknown-user' }}"
},
{
"name": "senderName",
"type": "string",
"value": "={{ $json.user_name || $json.body?.user_name || $json.sender || 'Unknown' }}"
},
{
"name": "messageText",
"type": "string",
"value": "={{ $json.text || $json.body?.text || $json.content || '' }}"
},
{
"name": "timestamp",
"type": "string",
"value": "={{ $json.ts || $json.body?.ts || new Date().toISOString() }}"
},
{
"name": "threadTs",
"type": "string",
"value": "={{ $json.thread_ts || $json.body?.thread_ts || '' }}"
},
{
"name": "platform",
"type": "string",
"value": "={{ $json.platform || 'slack' }}"
},
{
"name": "rawPayload",
"type": "string",
"value": "={{ JSON.stringify($json) }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "3688cce8-1697-4137-b6d9-48bbc30a490e",
"name": "Python - Noise Filter & Signal Tagging",
"type": "n8n-nodes-base.code",
"position": [
1744,
720
],
"parameters": {
"mode": "runOnceForEachItem",
"language": "python",
"pythonCode": "import re\nimport json\n\nitem = _input.item['json']\ntext = item.get('messageText', '')\nsender = item.get('senderName', '')\n\n# --- Noise filters ---\nnoise_patterns = [\n r'^\\s*$', # blank\n r'^[+\\-:()\\[\\]\\s\\d]+$', # only symbols / reactions\n r'^(hi|hello|hey|thanks|thx|ok|okay|\ud83d\udc4d|lgtm|+1)[\\.!\\s]*$', # greetings\n r'^<@[A-Z0-9]+>\\s*$', # bare @mention\n]\n\nis_noise = any(re.match(p, text.strip(), re.IGNORECASE) for p in noise_patterns)\n\n# Bot / automated message heuristics\nis_bot = 'bot' in sender.lower() or sender.startswith('B') or text.startswith('Automated:')\n\n# Too short to carry semantic content\ntoo_short = len(text.strip()) < 20\n\nitem['isRelevant'] = not (is_noise or is_bot or too_short)\nitem['wordCount'] = len(text.split())\nitem['hasQuestion'] = '?' in text\nitem['hasDecisionKeyword'] = bool(re.search(\n r'\\b(decided|decision|agreed|approved|rejected|choosing|we will|let\\'s go with|final|confirmed)\\b',\n text, re.IGNORECASE\n))\nitem['hasActionItem'] = bool(re.search(\n r'\\b(action|todo|to-do|will do|I\\'ll|follow.?up|deadline|by [A-Z][a-z]+day|assigned to)\\b',\n text, re.IGNORECASE\n))\n\nreturn item\n"
},
"typeVersion": 2
},
{
"id": "1495610a-247b-4430-994f-689d2a3e6c14",
"name": "Filter - Keep Relevant Messages Only",
"type": "n8n-nodes-base.filter",
"position": [
1984,
720
],
"parameters": {
"options": {},
"conditions": {
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $json.isRelevant }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "9e13a989-8eee-4073-ac54-cfead5e62227",
"name": "Wait - API Rate Limit Buffer",
"type": "n8n-nodes-base.wait",
"position": [
2224,
720
],
"parameters": {},
"typeVersion": 1
},
{
"id": "8a8ea0ef-3575-4703-8f23-a083002d17a0",
"name": "AI - Entity & Relationship Extractor",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2464,
608
],
"parameters": {
"text": "=You are an expert knowledge graph builder specialising in organisational intelligence.\n\nAnalyse the following internal chat message and extract ALL entities and relationships present.\n\n## Message Metadata\n- Platform: {{ $json.platform }}\n- Channel: {{ $json.channelName }}\n- Sender: {{ $json.senderName }} (ID: {{ $json.senderId }})\n- Timestamp: {{ $json.timestamp }}\n- Thread: {{ $json.threadTs || 'standalone' }}\n- Has Decision Keyword: {{ $json.hasDecisionKeyword }}\n- Has Action Item: {{ $json.hasActionItem }}\n\n## Message Text\n{{ $json.messageText }}\n\n---\n\nRespond ONLY with a valid JSON object (no markdown, no backticks, no preamble) using this exact schema:\n\n{\n \"entities\": [\n {\n \"id\": \"<slug-id e.g. person_alice_chen>\",\n \"type\": \"<PERSON | TOPIC | DECISION | ACTION_ITEM | PROJECT | TECHNOLOGY | MEETING | TEAM>\",\n \"label\": \"<human-readable name>\",\n \"properties\": {\n \"<key>\": \"<value>\"\n }\n }\n ],\n \"relationships\": [\n {\n \"from\": \"<entity id>\",\n \"to\": \"<entity id>\",\n \"type\": \"<DISCUSSED | DECIDED | ASSIGNED | DEPENDS_ON | PARTICIPATED_IN | MENTIONED | OWNS | BLOCKED_BY | FOLLOWS_UP>\",\n \"properties\": {\n \"confidence\": 0.0,\n \"timestamp\": \"\",\n \"context\": \"\"\n }\n }\n ],\n \"summary\": \"<One sentence summary of the message's core contribution>\",\n \"messageType\": \"<DECISION | ACTION_ITEM | DISCUSSION | INFORMATION | QUESTION | ANNOUNCEMENT>\",\n \"importance\": <1-5 integer, 5 = highest>\n}\n\nExtract every person mentioned (by name or @mention), every project/technology/topic referenced, any explicit decisions made, and any action items assigned. Be thorough.",
"options": {},
"promptType": "define"
},
"typeVersion": 1.6
},
{
"id": "90f1dd31-85d1-424b-b888-8b396c6ca1b4",
"name": "AI - Relationship Enrichment & Weighting",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2432,
1104
],
"parameters": {
"text": "=You are a knowledge graph relationship specialist.\n\nGiven the extracted entity graph below, perform a SECOND PASS to:\n1. Identify any IMPLICIT relationships not already captured (e.g. shared project context, temporal sequences, decision dependencies)\n2. Assign semantic weight scores to each relationship (0.0 \u2013 1.0)\n3. Detect if this message is part of a decision chain (references a prior decision or spawns a follow-up)\n4. Tag the overall discussion thread category\n\n## Extracted Graph from First Pass\n{{ $json.output }}\n\n## Channel Context\nChannel: {{ $json.channelName }}\nPlatform: {{ $json.platform }}\nMessage Type Signal: {{ $json.messageType || 'unknown' }}\n\n---\n\nRespond ONLY with a valid JSON object (no markdown, no backticks) using this schema:\n\n{\n \"enrichedRelationships\": [\n {\n \"from\": \"<entity id>\",\n \"to\": \"<entity id>\",\n \"type\": \"<relationship type>\",\n \"weight\": 0.0,\n \"isImplicit\": true,\n \"rationale\": \"<why this relationship exists>\"\n }\n ],\n \"decisionChain\": {\n \"isPartOfChain\": false,\n \"chainId\": \"\",\n \"position\": \"<INITIATING | CONTINUING | CONCLUDING | STANDALONE>\"\n },\n \"threadCategory\": \"<TECHNICAL | STRATEGIC | OPERATIONAL | SOCIAL | ADMINISTRATIVE>\",\n \"keyInsight\": \"<Most important organisational insight extracted from this message>\"\n}",
"options": {},
"promptType": "define"
},
"typeVersion": 1.6
},
{
"id": "94bf64f4-f292-4628-aa89-f1dfb8d05e4b",
"name": "JS - Merge & Structure Knowledge Graph",
"type": "n8n-nodes-base.code",
"position": [
2864,
720
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\n\n// Parse first-pass AI output\nlet firstPass = {};\ntry {\n const raw = item.output || item.text || '{}';\n const cleaned = raw.replace(/```json|```/g, '').trim();\n firstPass = JSON.parse(cleaned);\n} catch(e) {\n firstPass = { entities: [], relationships: [], summary: 'parse error', messageType: 'UNKNOWN', importance: 1 };\n}\n\n// Parse second-pass AI output (enrichment node result is in a different field)\n// In a real flow this node would receive merged context; here we gracefully default\nlet secondPass = {};\ntry {\n const raw2 = item.enrichedOutput || item.enrichmentOutput || '{}';\n const cleaned2 = raw2.replace(/```json|```/g, '').trim();\n secondPass = JSON.parse(cleaned2);\n} catch(e) {\n secondPass = { enrichedRelationships: [], decisionChain: { isPartOfChain: false, chainId: '', position: 'STANDALONE' }, threadCategory: 'OPERATIONAL', keyInsight: '' };\n}\n\n// Build canonical graph ID for this message\nconst graphId = `msg_${item.channelId || 'ch'}_${(item.timestamp || Date.now()).toString().replace(/[^a-zA-Z0-9]/g, '_')}`;\n\n// Merge all relationships\nconst allRelationships = [\n ...(firstPass.relationships || []),\n ...(secondPass.enrichedRelationships || []).map(r => ({ ...r, source: 'enrichment' }))\n];\n\n// Add the sender as a PERSON entity if not already present\nconst senderEntityId = `person_${(item.senderName || 'unknown').toLowerCase().replace(/\\s+/g, '_')}`;\nconst hasSender = (firstPass.entities || []).some(e => e.id === senderEntityId || (e.type === 'PERSON' && e.label === item.senderName));\nif (!hasSender && item.senderName) {\n (firstPass.entities || []).push({\n id: senderEntityId,\n type: 'PERSON',\n label: item.senderName,\n properties: { userId: item.senderId, platform: item.platform }\n });\n}\n\n// Add channel as a TEAM/CHANNEL entity\nconst channelEntityId = `channel_${(item.channelName || 'unknown').toLowerCase().replace(/\\s+/g, '_')}`;\n(firstPass.entities || []).push({\n id: channelEntityId,\n type: 'TEAM',\n label: `#${item.channelName || 'unknown'}`,\n properties: { channelId: item.channelId, platform: item.platform }\n});\n\n// Link sender \u2192 channel\nallRelationships.push({\n from: senderEntityId,\n to: channelEntityId,\n type: 'PARTICIPATED_IN',\n properties: { timestamp: item.timestamp, confidence: 1.0 }\n});\n\n// Final knowledge graph object\nconst knowledgeGraph = {\n graphId,\n messageId: item.messageId || graphId,\n timestamp: item.timestamp,\n platform: item.platform,\n channelName: item.channelName,\n senderName: item.senderName,\n messageText: item.messageText,\n messageType: firstPass.messageType || 'DISCUSSION',\n importance: firstPass.importance || 1,\n summary: firstPass.summary || '',\n threadCategory: secondPass.threadCategory || 'OPERATIONAL',\n keyInsight: secondPass.keyInsight || '',\n decisionChain: secondPass.decisionChain || { isPartOfChain: false, position: 'STANDALONE' },\n graph: {\n nodes: (firstPass.entities || []).map(e => ({\n id: e.id,\n type: e.type,\n label: e.label,\n properties: e.properties || {}\n })),\n edges: allRelationships.map((r, idx) => ({\n id: `edge_${graphId}_${idx}`,\n from: r.from,\n to: r.to,\n type: r.type,\n weight: r.weight || r.properties?.confidence || 0.7,\n isImplicit: r.isImplicit || false,\n properties: r.properties || {},\n rationale: r.rationale || ''\n }))\n },\n meta: {\n nodeCount: (firstPass.entities || []).length,\n edgeCount: allRelationships.length,\n processedAt: new Date().toISOString(),\n workflowVersion: '1.0.0'\n }\n};\n\nreturn { json: { ...item, knowledgeGraph, graphId, isValid: knowledgeGraph.graph.nodes.length > 0 } };\n"
},
"typeVersion": 2
},
{
"id": "04860da3-1334-4d14-848c-15c868322a0c",
"name": "JS - Validate Graph Integrity",
"type": "n8n-nodes-base.code",
"position": [
3200,
720
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nconst kg = item.knowledgeGraph;\n\nconst errors = [];\nconst warnings = [];\n\n// Required top-level fields\nif (!kg.graphId) errors.push('Missing graphId');\nif (!kg.timestamp) errors.push('Missing timestamp');\nif (!kg.graph) errors.push('Missing graph object');\n\nif (kg.graph) {\n if (!Array.isArray(kg.graph.nodes)) errors.push('graph.nodes must be an array');\n if (!Array.isArray(kg.graph.edges)) errors.push('graph.edges must be an array');\n\n const nodeIds = new Set((kg.graph.nodes || []).map(n => n.id));\n\n // Every edge must reference existing node IDs\n (kg.graph.edges || []).forEach((edge, i) => {\n if (!nodeIds.has(edge.from)) warnings.push(`Edge ${i} references unknown from-node: ${edge.from}`);\n if (!nodeIds.has(edge.to)) warnings.push(`Edge ${i} references unknown to-node: ${edge.to}`);\n if (typeof edge.weight !== 'number' || edge.weight < 0 || edge.weight > 1) {\n edge.weight = 0.5; // auto-correct to neutral\n warnings.push(`Edge ${i} had invalid weight; defaulted to 0.5`);\n }\n });\n\n // Node type whitelist\n const validNodeTypes = ['PERSON','TOPIC','DECISION','ACTION_ITEM','PROJECT','TECHNOLOGY','MEETING','TEAM'];\n (kg.graph.nodes || []).forEach((node, i) => {\n if (!validNodeTypes.includes(node.type)) {\n warnings.push(`Node ${i} (${node.id}) has non-standard type: ${node.type}`);\n }\n });\n\n if (kg.graph.nodes.length === 0) warnings.push('Graph has zero nodes \u2014 may indicate extraction failure');\n}\n\nconst isValid = errors.length === 0;\n\nreturn {\n json: {\n ...item,\n validation: { isValid, errors, warnings, checkedAt: new Date().toISOString() }\n }\n};\n"
},
"typeVersion": 2
},
{
"id": "2f3e7d84-b537-4df6-b2ca-9c164020e6c6",
"name": "Filter - Valid Graphs Only",
"type": "n8n-nodes-base.filter",
"position": [
3392,
720
],
"parameters": {
"options": {},
"conditions": {
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $json.validation.isValid }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "3e20ffcd-451d-473c-9c84-f603c3ad3157",
"name": "Neo4j - Upsert Nodes & Edges",
"type": "n8n-nodes-base.httpRequest",
"position": [
3872,
608
],
"parameters": {
"url": "https://YOUR_NEO4J_HOST/db/neo4j/tx/commit",
"method": "POST",
"options": {},
"jsonBody": "={\n \"statements\": [\n {\n \"statement\": \"UNWIND $nodes AS node MERGE (n:Entity {id: node.id}) SET n.type = node.type, n.label = node.label, n.updatedAt = datetime() SET n += node.properties\",\n \"parameters\": { \"nodes\": {{ JSON.stringify($json.knowledgeGraph.graph.nodes) }} }\n },\n {\n \"statement\": \"UNWIND $edges AS edge MATCH (a:Entity {id: edge.from}), (b:Entity {id: edge.to}) MERGE (a)-[r:RELATES {id: edge.id}]->(b) SET r.type = edge.type, r.weight = edge.weight, r.isImplicit = edge.isImplicit, r.updatedAt = datetime()\",\n \"parameters\": { \"edges\": {{ JSON.stringify($json.knowledgeGraph.graph.edges) }} }\n },\n {\n \"statement\": \"MERGE (m:Message {graphId: $graphId}) SET m.timestamp = $timestamp, m.messageType = $messageType, m.importance = $importance, m.summary = $summary, m.channelName = $channelName, m.senderName = $senderName, m.threadCategory = $threadCategory, m.keyInsight = $keyInsight, m.processedAt = datetime()\",\n \"parameters\": {\n \"graphId\": \"{{ $json.graphId }}\",\n \"timestamp\": \"{{ $json.knowledgeGraph.timestamp }}\",\n \"messageType\": \"{{ $json.knowledgeGraph.messageType }}\",\n \"importance\": {{ $json.knowledgeGraph.importance }},\n \"summary\": \"{{ $json.knowledgeGraph.summary }}\",\n \"channelName\": \"{{ $json.knowledgeGraph.channelName }}\",\n \"senderName\": \"{{ $json.knowledgeGraph.senderName }}\",\n \"threadCategory\": \"{{ $json.knowledgeGraph.threadCategory }}\",\n \"keyInsight\": \"{{ $json.knowledgeGraph.keyInsight }}\"\n }\n }\n ]\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json;charset=UTF-8"
}
]
}
},
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "368d4ef0-7aa7-4abe-81dc-0c1763ac4361",
"name": "Google Sheets - Append Audit Log Row",
"type": "n8n-nodes-base.httpRequest",
"position": [
3872,
816
],
"parameters": {
"url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/KnowledgeGraph!A1:append?valueInputOption=USER_ENTERED",
"method": "POST",
"options": {},
"jsonBody": "={\n \"values\": [[\n \"{{ $json.knowledgeGraph.meta.processedAt }}\",\n \"{{ $json.graphId }}\",\n \"{{ $json.knowledgeGraph.channelName }}\",\n \"{{ $json.knowledgeGraph.senderName }}\",\n \"{{ $json.knowledgeGraph.messageType }}\",\n \"{{ $json.knowledgeGraph.threadCategory }}\",\n {{ $json.knowledgeGraph.importance }},\n {{ $json.knowledgeGraph.meta.nodeCount }},\n {{ $json.knowledgeGraph.meta.edgeCount }},\n \"{{ $json.knowledgeGraph.summary }}\",\n \"{{ $json.knowledgeGraph.keyInsight }}\",\n \"{{ $json.knowledgeGraph.decisionChain.position }}\",\n \"{{ $json.validation.warnings.join(' | ') }}\"\n ]]\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "oAuth2"
},
"typeVersion": 4.2
},
{
"id": "f4e3ab1a-91d2-4487-9c5f-1f95497fd898",
"name": "JS - Build Success Response",
"type": "n8n-nodes-base.code",
"position": [
4160,
720
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\n\nreturn {\n json: {\n status: 'success',\n graphId: item.graphId,\n nodesWritten: item.knowledgeGraph?.meta?.nodeCount || 0,\n edgesWritten: item.knowledgeGraph?.meta?.edgeCount || 0,\n messageType: item.knowledgeGraph?.messageType,\n importance: item.knowledgeGraph?.importance,\n processedAt: item.knowledgeGraph?.meta?.processedAt,\n warnings: item.validation?.warnings || []\n }\n};\n"
},
"typeVersion": 2
},
{
"id": "8392c13e-ec57-4a1a-8b17-dcd0a235965c",
"name": "Webhook Response - 200 OK",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
4400,
720
],
"parameters": {
"options": {
"responseCode": 200
},
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}"
},
"typeVersion": 1
},
{
"id": "891c6038-4d5d-4b05-a903-5cc315d0a185",
"name": "Anthropic Claude - Entity Extractor Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
2448,
800
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-20250514"
},
"options": {}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "98b7879f-53e7-4fd1-ac08-cd85aef93072",
"name": "Anthropic Claude - Relationship Enricher Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
2400,
1328
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-20250514"
},
"options": {}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "b28d30db-b709-48e1-9328-8caeb4b340a4",
"name": "Wait For Data",
"type": "n8n-nodes-base.wait",
"position": [
3024,
720
],
"parameters": {
"amount": 15
},
"typeVersion": 1.1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "3fc48a32-a99b-4436-a57d-9376d7b36c8f",
"connections": {
"Wait For Data": {
"main": [
[
{
"node": "JS - Validate Graph Integrity",
"type": "main",
"index": 0
}
]
]
},
"Normalise Message Payload": {
"main": [
[
{
"node": "Python - Noise Filter & Signal Tagging",
"type": "main",
"index": 0
}
]
]
},
"Filter - Valid Graphs Only": {
"main": [
[
{
"node": "Neo4j - Upsert Nodes & Edges",
"type": "main",
"index": 0
},
{
"node": "Google Sheets - Append Audit Log Row",
"type": "main",
"index": 0
}
]
]
},
"JS - Build Success Response": {
"main": [
[
{
"node": "Webhook Response - 200 OK",
"type": "main",
"index": 0
}
]
]
},
"Neo4j - Upsert Nodes & Edges": {
"main": [
[
{
"node": "JS - Build Success Response",
"type": "main",
"index": 0
}
]
]
},
"Wait - API Rate Limit Buffer": {
"main": [
[
{
"node": "AI - Entity & Relationship Extractor",
"type": "main",
"index": 0
},
{
"node": "AI - Relationship Enrichment & Weighting",
"type": "main",
"index": 0
}
]
]
},
"JS - Validate Graph Integrity": {
"main": [
[
{
"node": "Filter - Valid Graphs Only",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Chat Message Ingest": {
"main": [
[
{
"node": "Normalise Message Payload",
"type": "main",
"index": 0
}
]
]
},
"Schedule - Poll Slack Every 15 min": {
"main": [
[
{
"node": "Normalise Message Payload",
"type": "main",
"index": 0
}
]
]
},
"AI - Entity & Relationship Extractor": {
"main": [
[
{
"node": "JS - Merge & Structure Knowledge Graph",
"type": "main",
"index": 0
}
]
]
},
"Filter - Keep Relevant Messages Only": {
"main": [
[
{
"node": "Wait - API Rate Limit Buffer",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets - Append Audit Log Row": {
"main": [
[
{
"node": "JS - Build Success Response",
"type": "main",
"index": 0
}
]
]
},
"JS - Merge & Structure Knowledge Graph": {
"main": [
[
{
"node": "Wait For Data",
"type": "main",
"index": 0
}
]
]
},
"Python - Noise Filter & Signal Tagging": {
"main": [
[
{
"node": "Filter - Keep Relevant Messages Only",
"type": "main",
"index": 0
}
]
]
},
"AI - Relationship Enrichment & Weighting": {
"main": [
[
{
"node": "JS - Merge & Structure Knowledge Graph",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Claude - Entity Extractor Model": {
"ai_languageModel": [
[
{
"node": "AI - Entity & Relationship Extractor",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Anthropic Claude - Relationship Enricher Model": {
"ai_languageModel": [
[
{
"node": "AI - Relationship Enrichment & Weighting",
"type": "ai_languageModel",
"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.
anthropicApihttpBasicAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow ingests chat messages via a webhook or a 15-minute schedule, filters out low-signal content, uses Anthropic Claude to extract entities and relationships, and stores the resulting knowledge graph in Neo4j while appending an audit log row to Google Sheets. Receives a…
Source: https://n8n.io/workflows/16231/ — 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.
⏺ 🚀 How it works
Lead Pipeline v3.0. Uses httpRequest, agent, lmChatAnthropic, toolThink. Webhook trigger; 77 nodes.
What if AI didn't just write content—but actually thought about how to write it? This n8n workflow revolutionizes content creation by deploying multiple specialized AI agents that handle every aspect
Fully automates your service order pipeline from incoming booking to supplier confirmation — with built-in SLA enforcement and automatic escalation if a supplier goes silent. 📥 Receives orders via web
A complete WhatsApp AI chatbot that handles class bookings, cancellations, FAQ responses, schedule lookups, location queries, waitlist management, booking reminders, and staff notifications — all thro