This workflow follows the Agent → Execute Workflow Trigger 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 →
{
"name": "\ud83e\udd16 DMO Claw",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "dmo-claw",
"authentication": "headerAuth",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-trigger",
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
-16
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Extract message from OpenWebUI webhook payload\n// Payload: { messages: [{role, content}], user: {email, name} }\nconst body = $('Webhook Trigger').first().json.body;\nconst messages = body.messages || [];\nconst lastUserMsg = messages.filter(m => m.role === 'user').pop();\nconst userMessage = lastUserMsg?.content || '';\nconst userEmail = body.user?.email || '';\nconst userName = body.user?.name || 'Unknown';\n\nreturn [{\n json: {\n userMessage,\n userEmail,\n userName\n }\n}];"
},
"id": "normalize-message",
"name": "Normalize Message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
224,
-16
]
},
{
"parameters": {
"inputSource": "passthrough"
},
"id": "scheduled-task-trigger",
"name": "Scheduled Task Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
300
]
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst message = raw.message || '';\nconst userId = String(raw.user_id || 'system');\n// Extract email from oi: prefix\nconst userEmail = userId.startsWith('oi:') ? userId.slice(3) : userId;\nconst userName = 'System Task';\nconst source = raw.source || 'scheduled_task';\n\nreturn [{\n json: {\n userMessage: message,\n userEmail,\n userName,\n source\n }\n}];"
},
"id": "format-task-input",
"name": "Format Task Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
224,
300
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nreturn [{ json: data }];"
},
"id": "merge-input",
"name": "Merge Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
448,
140
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT key, content FROM soul ORDER BY key",
"options": {
"queryBatching": "independently"
}
},
"id": "load-soul",
"name": "Load Soul",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
672,
140
],
"alwaysOutputData": true,
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT key, content FROM agents WHERE key NOT LIKE 'persona:%' ORDER BY key",
"options": {
"queryBatching": "independently"
}
},
"id": "load-agents",
"name": "Load Agents Config",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
672,
-16
],
"alwaysOutputData": true,
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT name, display_name, timezone, preferences, context, setup_done FROM user_profiles WHERE user_id = 'oi:{{ $('Merge Input').first().json.userEmail }}' LIMIT 1",
"options": {
"queryBatching": "independently"
}
},
"id": "load-user",
"name": "Load User Profile",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
896,
-16
],
"alwaysOutputData": true,
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT role, name FROM dmo_users WHERE oi_email = '{{ $('Merge Input').first().json.userEmail }}' AND active = true LIMIT 1",
"options": {
"queryBatching": "independently"
}
},
"id": "load-user-role",
"name": "Load User Role",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1120,
-16
],
"alwaysOutputData": true,
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT role, content FROM conversations WHERE session_id = 'oi:{{ $('Merge Input').first().json.userEmail }}' ORDER BY created_at DESC LIMIT 20",
"options": {
"queryBatching": "independently"
}
},
"id": "load-history",
"name": "Load Conversation History",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1344,
-16
],
"alwaysOutputData": true,
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT server_name, mcp_url, description, tools FROM mcp_registry WHERE active = true ORDER BY server_name",
"options": {
"queryBatching": "independently"
}
},
"id": "load-mcp-servers",
"name": "Load MCP Servers",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1456,
-16
],
"alwaysOutputData": true,
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"continueOnFail": true
},
{
"parameters": {
"jsCode": "const soul = $('Load Soul').all().map(i => i.json);\nconst agentsRaw = $('Load Agents Config').all().map(i => i.json);\nconst agents = [...new Map(agentsRaw.map(a => [a.key, a])).values()];\nconst user = $('Load User Profile').first()?.json || {};\nconst userRole = $('Load User Role').first()?.json || {};\nconst role = userRole.role || 'readonly';\nconst roleName = userRole.name || '';\nconst historyRaw = $('Load Conversation History').all().map(i => i.json);\nconst seenHist = new Set();\nconst history = historyRaw.filter(h => { const k = h.role + ':' + h.content; if (seenHist.has(k)) return false; seenHist.add(k); return true; });\nconst userMessage = $('Merge Input').first().json.userMessage || '';\nconst userEmail = $('Merge Input').first().json.userEmail || 'unknown';\nconst userName = $('Merge Input').first().json.userName || 'Unknown';\n\nconst baseUrl = '{{SUPABASE_URL}}';\nconst serviceKey = '{{SUPABASE_SERVICE_KEY}}';\nconst dbHeaders = { 'apikey': serviceKey, 'Authorization': 'Bearer ' + serviceKey, 'Content-Type': 'application/json' };\nlet notifications = [];\ntry {\n notifications = await helpers.httpRequest({ method: 'GET', url: baseUrl + '/rest/v1/notifications?read=eq.false&or=(recipient_email.eq.' + encodeURIComponent(userEmail) + ',recipient_email.eq.all)&order=created_at.desc&limit=10', headers: dbHeaders });\n if (notifications && notifications.length > 0) {\n const ids = notifications.map(n => n.id);\n await helpers.httpRequest({ method: 'PATCH', url: baseUrl + '/rest/v1/notifications?id=in.(' + ids.join(',') + ')', headers: { ...dbHeaders, 'Prefer': 'return=minimal' }, body: JSON.stringify({ read: true }) });\n }\n} catch(e) {}\n\nconst soulText = soul.map(s => '## ' + s.key + '\\n' + s.content).join('\\n\\n');\n\n// Filter agents by role: exclude write-only configs for non-admins\nconst adminOnlyKeys = ['member_management_write'];\nconst filteredAgents = agents.filter(a => {\n if (adminOnlyKeys.includes(a.key) && role !== 'admin') return false;\n return true;\n});\n// Build dynamic MCP server list\nconst mcpRaw = $('Load MCP Servers').all().map(i => i.json);\nconst seenMcp = new Set();\nconst mcpServers = mcpRaw.filter(s => { if (!s.server_name || seenMcp.has(s.server_name)) return false; seenMcp.add(s.server_name); return true; });\nlet mcpServerList = '';\nif (mcpServers.length > 0) {\n mcpServerList = '\\n\\n## Available MCP Skills:\\n' + mcpServers.map(s => {\n const tools = Array.isArray(s.tools) ? s.tools.join(', ') : s.tools;\n return '- ' + s.server_name + ': ' + s.mcp_url + ' (tools: ' + tools + ')' + (s.description ? ' \u2014 ' + s.description : '');\n }).join('\\n');\n}\n\nconst agentText = filteredAgents.map(a => {\n if (a.key === 'mcp_instructions') {\n const cleaned = a.content.replace(/\\n## (?:Available MCP (?:Servers|Skills)|Currently installed MCP Skills)[\\s\\S]*?(?=\\n## |$)/, '');\n return '## ' + a.key + '\\n' + cleaned + mcpServerList;\n }\n return '## ' + a.key + '\\n' + a.content;\n}).join('\\n\\n');\n\nconst userText = user.display_name ? 'Name: ' + user.display_name + ' (' + user.name + ')\\nTimezone: ' + user.timezone + '\\nKontext: ' + user.context + '\\nPrefs: ' + JSON.stringify(user.preferences) + '\\nSetup done: ' + (user.setup_done !== false ? 'true' : 'false') : 'Unbekannter User';\n\nconst roleContextMap = { marketing: 'Rolle: Marketing (' + roleName + ')\\nDu hilfst bei Social Media, Content-Erstellung und Posting.\\nBevorzugte Tools: create_instagram_post, schedule_post, alpine_weather\\nNutze Bewertungs-Tools nur wenn explizit danach gefragt wird.', member_relations: 'Rolle: Mitgliederbetreuung (' + roleName + ')\\nDu hilfst bei Bewertungen, Mitgliedsbetrieben und deren Anliegen.\\nBevorzugte Tools: query_reviews, review_summary, alpine_weather\\nNutze Posting-Tools nur wenn explizit danach gefragt wird.', admin: 'Rolle: Admin (' + roleName + ')\\nVollzugriff auf alle Tools und Funktionen.', readonly: 'Rolle: Lesezugriff\\nDu kannst Informationen abrufen, aber keine Aktionen ausfuehren.' };\nconst roleContext = roleContextMap[role] || roleContextMap.readonly;\n\nconst historyText = history.reverse().map(h => h.role + ': ' + h.content).join('\\n');\n\nlet notifText = '';\nif (notifications && notifications.length > 0) {\n notifText = '\\n\\n# UNGELESENE BENACHRICHTIGUNGEN\\nWICHTIG: Erwaehne diese Benachrichtigungen proaktiv am Anfang deiner Antwort!\\n';\n for (const n of notifications) { notifText += '- [' + n.type + '] ' + n.title + ': ' + (n.body || '') + '\\n'; }\n}\n\nconst now = $now.setZone(user.timezone || 'UTC').toFormat('cccc, dd LLLL yyyy, HH:mm');\n\nconst systemPrompt = '# CURRENT TIME\\n' + now + '\\n\\n# SOUL\\n' + soulText + '\\n\\n# AGENT CONFIG\\n' + agentText + '\\n\\n# USER ROLE\\n' + roleContext + '\\n\\n# USER PROFILE\\n' + userText + notifText + '\\n\\n# RECENT CONVERSATION\\n' + historyText;\n\nreturn [{ json: { systemPrompt, userMessage, userEmail, userName, userId: userEmail, sessionId: 'oi:' + userEmail } }];"
},
"id": "build-prompt",
"name": "Build System Prompt",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1680,
-16
]
},
{
"parameters": {
"promptType": "define",
"text": "={{ $json.userMessage }}",
"options": {
"systemMessage": "={{ $json.systemPrompt }}",
"maxIterations": 10
}
},
"id": "ai-agent",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
2424,
-16
]
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-6",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.6"
},
"options": {
"maxTokensToSample": 8192,
"temperature": 0.7
}
},
"id": "claude-llm",
"name": "Claude",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
1792,
208
],
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Save conversation to Supabase\nconst userMessage = $('Build System Prompt').first().json.userMessage;\nconst assistantMessage = $('AI Agent').first().json.output;\nconst sessionId = $('Build System Prompt').first().json.sessionId;\nconst userId = $('Build System Prompt').first().json.userId;\n\n// Mark onboarding as done after first response\nconst setupDone = $('Load User Profile').first()?.json?.setup_done;\nconst markSetupDone = setupDone === false;\n\nreturn [{\n json: {\n userMessage,\n assistantMessage,\n sessionId,\n userId,\n markSetupDone\n }\n}];"
},
"id": "prepare-save",
"name": "Prepare Save",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3408,
-16
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO conversations (session_id, user_id, role, content) VALUES ('{{ $json.sessionId }}', '{{ $json.userId }}', 'user', '{{ $json.userMessage.replace(/'/g, \"''\") }}'); INSERT INTO conversations (session_id, user_id, role, content) VALUES ('{{ $json.sessionId }}', '{{ $json.userId }}', 'assistant', '{{ $json.assistantMessage.replace(/'/g, \"''\") }}'); {{ $json.markSetupDone ? \"UPDATE user_profiles SET setup_done = true WHERE user_id = 'oi:\" + $json.userId + \"';\" : '' }}",
"options": {
"queryBatching": "independently"
}
},
"id": "save-conversation",
"name": "Save Conversation",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
3632,
-208
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO memory_daily (date, content, role, user_id) VALUES (CURRENT_DATE, '{{ ($json.userMessage + ' \u2192 ' + $json.assistantMessage).replace(/'/g, \"''\") }}', 'interaction', '{{ $json.userId }}')",
"options": {
"queryBatching": "independently"
}
},
"id": "save-daily-log",
"name": "Save Daily Log",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
3632,
176
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-webhook",
"leftValue": "={{ $('Merge Input').first().json.source }}",
"rightValue": "scheduled_task",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
],
"combinator": "and"
}
},
"id": "route-response",
"name": "Route Response",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3432,
-16
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ output: $json.assistantMessage }) }}"
},
"id": "webhook-reply",
"name": "Webhook Reply",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
3632,
-80
]
},
{
"parameters": {
"description": "Search long-term memory for relevant information. Input: JSON with search_query (string). Automatically scoped to the current user's personal memories + shared org memories.",
"workflowId": {
"__rl": true,
"value": "REPLACE_MEMORY_SEARCH_ID",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"sessionId": "={{ $('Build System Prompt').item.json.sessionId }}"
},
"matchingColumns": [],
"schema": [
{
"id": "sessionId",
"displayName": "sessionId",
"type": "string",
"required": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"id": "tool-memory-search",
"name": "Memory Search",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
1920,
208
]
},
{
"parameters": {
"description": "Save important info to long-term memory. Input: JSON with content (string), category (general/decision/preference/contact/project), importance (1-10), scope ('user' or 'org', default 'user'). scope 'user': personal info (preferences, habits, facts about the user). scope 'org': organization/DMO knowledge (business facts, regional info, member data, shared decisions). User identity is handled automatically.",
"workflowId": {
"__rl": true,
"value": "REPLACE_MEMORY_SAVE_ID",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"sessionId": "={{ $('Build System Prompt').item.json.sessionId }}"
},
"matchingColumns": [],
"schema": [
{
"id": "sessionId",
"displayName": "sessionId",
"type": "string",
"required": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"id": "tool-memory-save",
"name": "Memory Save",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
2048,
208
]
},
{
"parameters": {
"description": "Delete a memory entry by ID. Input: JSON with id (number, required). Always memory_search first to get the ID. Only deletes memories the current user owns (user_id matches sessionId) or org-shared memories (user_id IS NULL). Use when information is wrong, obsolete, or the user explicitly asks to forget something.",
"jsCode": "const sessionId = $('Build System Prompt').first().json.sessionId || '';\nlet input;\ntry { input = JSON.parse(query); } catch(e) { input = {}; }\nconst id = input.id;\nif (!id) return JSON.stringify({ error: 'id is required. Use memory_search first to find the ID.' });\nif (!sessionId) return JSON.stringify({ error: 'sessionId missing from context \u2014 cannot enforce user scope.' });\nconst scopeFilter = `or=(user_id.is.null,user_id.eq.${encodeURIComponent(sessionId)})`;\nconst result = await helpers.httpRequest({\n method: 'DELETE',\n url: `{{SUPABASE_URL}}/rest/v1/memory_long?id=eq.${id}&${scopeFilter}`,\n headers: {\n 'apikey': '{{SUPABASE_SERVICE_KEY}}',\n 'Content-Type': 'application/json',\n 'Prefer': 'return=representation'\n },\n returnFullResponse: true\n});\nconst body = result.body;\nif (Array.isArray(body) && body.length === 0) return JSON.stringify({ error: `Memory with id ${id} not found, already deleted, or not owned by this user.` });\nreturn JSON.stringify({ success: true, deleted_id: id });"
},
"id": "tool-memory-delete",
"name": "Memory Delete",
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.2,
"position": [
2048,
336
]
},
{
"parameters": {
"description": "Update an existing memory entry by ID. Input: JSON with id (number, required), and optional fields to update: content (string), category (general/decision/preference/contact/project), importance (1-10), tags (string array), entity_name (string), source (string). Always memory_search first to get the ID. Only updates memories the current user owns (user_id matches sessionId) or org-shared memories (user_id IS NULL).",
"jsCode": "async function getEmbedding(text) {\n const cfgResp = await helpers.httpRequest({ method: 'GET', url: '{{SUPABASE_URL}}/rest/v1/tools_config?tool_name=eq.embedding&select=config,enabled', headers: { 'apikey': '{{SUPABASE_SERVICE_KEY}}', 'Content-Type': 'application/json' } });\n const cfg = (Array.isArray(cfgResp) && cfgResp.length > 0 && cfgResp[0].enabled) ? cfgResp[0].config : null;\n if (!cfg || !cfg.api_key) return null;\n const provider = cfg.provider || 'openai';\n const apiKey = cfg.api_key;\n const model = cfg.model || 'text-embedding-3-small';\n try {\n if (provider === 'openai') {\n const res = await helpers.httpRequest({ method: 'POST', url: 'https://api.openai.com/v1/embeddings', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model, input: text }) });\n return res.data[0].embedding;\n }\n if (provider === 'voyage') {\n const res = await helpers.httpRequest({ method: 'POST', url: 'https://api.voyageai.com/v1/embeddings', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model || 'voyage-3-lite', input: [text] }) });\n return res.data[0].embedding;\n }\n if (provider === 'ollama') {\n const ollamaUrl = cfg.ollama_url || 'http://localhost:11434';\n const res = await helpers.httpRequest({ method: 'POST', url: `${ollamaUrl}/api/embed`, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model || 'nomic-embed-text', input: text }) });\n return res.embeddings[0];\n }\n } catch(e) { return null; }\n return null;\n}\n\nconst sessionId = $('Build System Prompt').first().json.sessionId || '';\nlet input;\ntry { input = JSON.parse(query); } catch(e) { input = {}; }\nconst id = input.id;\nif (!id) return JSON.stringify({ error: 'id is required. Use memory_search first to find the ID.' });\nif (!sessionId) return JSON.stringify({ error: 'sessionId missing from context \u2014 cannot enforce user scope.' });\nconst patch = {};\nif (input.content) patch.content = input.content;\nif (input.category) patch.category = input.category;\nif (input.importance) patch.importance = input.importance;\nif (input.tags) patch.tags = Array.isArray(input.tags) ? input.tags.map(t => String(t).toLowerCase()) : input.tags;\nif (input.entity_name) patch.entity_name = input.entity_name;\nif (input.source) patch.source = input.source;\nif (Object.keys(patch).length === 0) return JSON.stringify({ error: 'Provide at least one field to update: content, category, importance, tags, entity_name, or source.' });\nif (input.content) {\n const embedding = await getEmbedding(input.content);\n if (embedding) patch.embedding = JSON.stringify(embedding);\n}\nconst scopeFilter = `or=(user_id.is.null,user_id.eq.${encodeURIComponent(sessionId)})`;\nconst result = await helpers.httpRequest({\n method: 'PATCH',\n url: `{{SUPABASE_URL}}/rest/v1/memory_long?id=eq.${id}&${scopeFilter}`,\n headers: {\n 'apikey': '{{SUPABASE_SERVICE_KEY}}',\n 'Content-Type': 'application/json',\n 'Prefer': 'return=representation'\n },\n body: JSON.stringify(patch),\n returnFullResponse: true\n});\nconst body = result.body;\nif (Array.isArray(body) && body.length === 0) return JSON.stringify({ error: `Memory with id ${id} not found or not owned by this user.` });\nreturn JSON.stringify({ success: true, updated_id: id, fields: Object.keys(patch).filter(k => k !== 'embedding'), vectorized: !!patch.embedding });"
},
"id": "tool-memory-update",
"name": "Memory Update",
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.2,
"position": [
2176,
336
]
},
{
"parameters": {
"description": "Make HTTP requests. Input: JSON with url (string), optional method (GET/POST/PATCH/DELETE), optional headers (object), optional body (object for POST/PATCH).",
"jsCode": "let input;\ntry { input = JSON.parse(query); } catch(e) { input = {url: query}; }\nif (!input.url) return 'Fehler: url ist Pflicht';\nconst opts = {\n method: input.method || 'GET',\n url: input.url,\n headers: input.headers || {}\n};\nif (input.body && ['POST','PATCH','PUT'].includes((input.method||'').toUpperCase())) {\n opts.body = input.body;\n if (!opts.headers['Content-Type']) opts.headers['Content-Type'] = 'application/json';\n}\ntry {\n const result = await helpers.httpRequest(opts);\n return typeof result === 'string' ? result.substring(0,4000) : JSON.stringify(result).substring(0,4000);\n} catch(e) {\n return 'HTTP Error: ' + e.message;\n}"
},
"id": "tool-http",
"name": "HTTP Tool",
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.2,
"position": [
2176,
208
]
},
{
"parameters": {
"description": "Search the web. Input: search query as string or JSON with q field. Returns top 5 results with title, URL, and snippet.",
"jsCode": "let input;\ntry { input = JSON.parse(query); } catch(e) { input = { q: query }; }\nconst q = input.q || input.query || query;\nconst result = await helpers.httpRequest({\n method: 'GET',\n url: `http://searxng:8080/search?q=${encodeURIComponent(q)}&format=json&language=de`,\n headers: { 'Accept': 'application/json' }\n});\nconst hits = (result.results || []).slice(0, 5).map(r =>\n `${r.title}\\n${r.url}\\n${r.content || ''}`\n).join('\\n\\n');\nreturn hits || 'Keine Ergebnisse gefunden.';"
},
"id": "tool-web-search",
"name": "Web Search",
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.2,
"position": [
2304,
208
]
},
{
"parameters": {
"description": "List or manage n8n workflows. Input: JSON with action (list/get/create) and optional workflowId.",
"jsCode": "let input;\ntry { input = JSON.parse(query); } catch(e) { input = {action:'list'}; }\nconst apiKey = '{{N8N_API_KEY}}';\nconst hdrs = {'X-N8N-API-KEY': apiKey, 'Content-Type': 'application/json'};\nlet result;\nif (input.action === 'list') {\n result = await helpers.httpRequest({method:'GET', url:'{{N8N_INTERNAL_URL}}/api/v1/workflows', headers: hdrs});\n} else if (input.action === 'get' && input.workflowId) {\n result = await helpers.httpRequest({method:'GET', url:'{{N8N_INTERNAL_URL}}/api/v1/workflows/'+input.workflowId, headers: hdrs});\n} else {\n return JSON.stringify({error: 'Use action: list or get'});\n}\nreturn JSON.stringify(result);"
},
"id": "tool-self-modify",
"name": "Self Modify",
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.2,
"position": [
2432,
208
]
},
{
"parameters": {
"description": "Set a timed reminder, scheduled task, or recurring action. IMPORTANT: Always include user_id \u2014 use the sessionId from the system prompt (format: oi:email@example.com, e.g. oi:friedemann.schuetz@posteo.de). NEVER use display names like 'oi:Petra'.\\nModes:\\n- Reminder (type='reminder'): writes a notification that the agent picks up in the next chat. Use for 'remind me at/in...'.\\n- Task (type='task'): the agent executes the instructions once at the given time and sends the result. Use for 'do X at Y o'clock'.\\n- Recurring (type='recurring'): creates a repeating scheduled action. Extra fields: name (label), instruction (what to do), schedule (object), action_type ('agent_task' or 'briefing', default 'agent_task'), notify_mode ('always' or 'on_change', default 'always'), timezone (default 'Europe/Berlin'). Schedule formats: {\"type\":\"interval\",\"minutes\":30} or {\"type\":\"daily\",\"time\":\"08:00\"} or {\"type\":\"weekly\",\"days\":[1,5],\"time\":\"09:00\"} (1=Mon..7=Sun). notify_mode: 'always' = always send result (use for briefings, reports). 'on_change' = only notify when something NEW is found (use for background monitoring).\\n- Management scheduled actions (recurring): action='list' (show all scheduled actions), action='enable'/'disable'/'delete' with target_id (ID of the action to manage).\\n- Management one-time reminders/tasks: action='list_reminders' (show pending one-time reminders + tasks), action='edit_reminder' with target_id and optional fields (time, message, type), action='delete_reminder' with target_id. Use these when the user asks about their reminders/tasks, wants to change the time/text of a reminder, or wants to cancel a reminder.\\nInput for reminder/task: time (ISO 8601), message (text), type, user_id. Input for recurring: see above plus user_id. Input for management: action, target_id, user_id.",
"workflowId": {
"__rl": true,
"value": "REPLACE_REMINDER_FACTORY_ID",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"id": "tool-reminder-wf",
"name": "Reminder",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
2560,
208
]
},
{
"parameters": {
"description": "Builds new n8n workflows for general automations and integrations. Use when the user explicitly wants to automate something or build an integration workflow. NOT for MCP servers \u2014 use mcp_builder for that. NOT as a fallback when other tools fail. Input: JSON with 'task' (description of the automation to build).",
"workflowId": {
"__rl": true,
"value": "REPLACE_WORKFLOW_BUILDER_ID",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"id": "tool-workflow-builder",
"name": "WorkflowBuilder",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
2688,
208
]
},
{
"parameters": {
"name": "mcp_builder",
"description": "Builds a new MCP Server workflow in n8n. ALWAYS use this when the user wants to build an MCP server, MCP tool, or API integration as MCP. NEVER use WorkflowBuilder for MCP. On error: report the error, do NOT fall back to WorkflowBuilder. Input: JSON with 'task' (description of what the server should do).",
"workflowId": {
"__rl": true,
"value": "REPLACE_MCP_BUILDER_ID",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"id": "mcp-builder-tool",
"name": "MCP Builder",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2,
"position": [
2816,
208
]
},
{
"parameters": {
"description": "Calls a tool on an MCP server. Use when the user asks something that an available MCP server can answer (weather, book lookup, etc.). Required parameters: mcp_url (server URL), tool_name (tool to call), arguments (JSON object with parameters). Check mcp_registry table for available servers and their tools.",
"jsCode": "const qType = typeof query;\nlet mcpUrl = '', toolName = '', args = {};\n\nif (qType === 'object' && query !== null) {\n mcpUrl = query.mcp_url || '';\n toolName = query.tool_name || '';\n args = query.arguments || {};\n} else if (qType === 'string') {\n try {\n const parsed = JSON.parse(query);\n mcpUrl = parsed.mcp_url || '';\n toolName = parsed.tool_name || '';\n args = parsed.arguments || {};\n } catch(e) {\n return 'Parse error: query=' + String(query).substring(0,200) + ' type=' + qType;\n }\n} else {\n return 'Unexpected query type: ' + qType + ' value: ' + String(query).substring(0,200);\n}\n\nif (typeof args === 'string') try { args = JSON.parse(args); } catch(e) {}\nif (!mcpUrl || !toolName) return 'Fehler: mcp_url=' + mcpUrl + ' tool_name=' + toolName + ' (query type was: ' + qType + ')';\n\nfunction parseSSE(raw) {\n const str = String(raw);\n let lines = str.split('\\n');\n if (lines.length <= 1) lines = str.split('\\\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n if (line.startsWith('data: ')) {\n return JSON.parse(line.substring(6));\n }\n }\n return null;\n}\n\ntry {\n let authHeader = null;\n try {\n const row = await helpers.httpRequest({method:'GET',url:'{{SUPABASE_URL}}/rest/v1/mcp_registry?mcp_url=eq.'+encodeURIComponent(mcpUrl)+'&select=auth_type,auth_token&limit=1',headers:{apikey:'{{SUPABASE_SERVICE_KEY}}',Authorization:'Bearer {{SUPABASE_SERVICE_KEY}}'},json:true});\n if (row && row[0] && row[0].auth_type && row[0].auth_type !== 'none' && row[0].auth_token) {\n authHeader = row[0].auth_type === 'bearer' ? 'Bearer '+row[0].auth_token : row[0].auth_token;\n }\n } catch(e) {}\n\n const hdrs = {'Content-Type':'application/json','Accept':'application/json, text/event-stream'};\n if (authHeader) hdrs['Authorization'] = authHeader;\n\n // 1. Initialize\n const init = await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',id:1,method:'initialize',params:{protocolVersion:'2024-11-05',capabilities:{},clientInfo:{name:'n8n-claw-mcp',version:'1.0'}}}),\n returnFullResponse: true, encoding: 'utf-8', json: false\n });\n const sid = init.headers && init.headers['mcp-session-id'];\n if (sid) hdrs['mcp-session-id'] = sid;\n\n // 2. Initialized notification\n await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'}),\n json: false\n });\n\n // 3. List tools to get schemas (auto-fill missing required params)\n const listResp = await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',id:2,method:'tools/list',params:{}}),\n json: false\n });\n const listResult = parseSSE(listResp);\n let toolSchema = null;\n if (listResult && listResult.result && listResult.result.tools) {\n const tool = listResult.result.tools.find(t => t.name === toolName);\n if (tool && tool.inputSchema) toolSchema = tool.inputSchema;\n }\n\n function schemaHint(problem) {\n if (!toolSchema) return null;\n return 'Tool \"' + toolName + '\" call rejected: ' + problem +\n '\\n\\nCorrect input schema:\\n' + JSON.stringify(toolSchema, null, 2) +\n '\\n\\nRetry the tool call with parameter names matching this schema exactly.';\n }\n\n if (toolSchema && toolSchema.properties) {\n const allowed = Object.keys(toolSchema.properties);\n const required = toolSchema.required || [];\n const unknown = Object.keys(args).filter(k => !allowed.includes(k));\n const missing = required.filter(r => args[r] === undefined || args[r] === null || args[r] === '');\n if (unknown.length > 0 || missing.length > 0) {\n const problems = [];\n if (unknown.length > 0) problems.push('unknown args [' + unknown.join(', ') + ']');\n if (missing.length > 0) problems.push('missing required args [' + missing.join(', ') + ']');\n return schemaHint(problems.join('; '));\n }\n }\n\n const resp = await helpers.httpRequest({\n method: 'POST', url: mcpUrl,\n headers: hdrs,\n body: JSON.stringify({jsonrpc:'2.0',id:3,method:'tools/call',params:{name:toolName,arguments:args}}),\n returnFullResponse: true, encoding: 'utf-8', json: false\n });\n\n const result = parseSSE(resp.body || resp);\n if (!result) return String(resp.body || resp);\n if (result.error) {\n return schemaHint('server error: ' + JSON.stringify(result.error)) || ('MCP Error: ' + JSON.stringify(result.error));\n }\n if (result.result && result.result.isError) {\n const errText = (result.result.content && result.result.content[0] && result.result.content[0].text) || JSON.stringify(result.result);\n return schemaHint('tool returned error: ' + errText) || ('MCP Tool Error: ' + errText);\n }\n return result.result?.content?.[0]?.text || JSON.stringify(result.result);\n} catch(e) {\n return 'MCP Error: ' + e.message;\n}"
},
"id": "mcp-client-code",
"name": "MCP Client",
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.2,
"position": [
2944,
208
]
},
{
"parameters": {
"description": "Manage tasks. Input: JSON with action and parameters.\n\nActions:\n- list: List tasks. Optional: status (pending/in_progress/done/cancelled), priority, due_before (ISO date)\n- create: Create task. Required: title. Optional: description, priority (low/medium/high/urgent), due_date (ISO 8601), parent_id (for subtasks), tags (array)\n- update: Update task. Required: id. Optional: title, description, status, priority, due_date, tags\n- delete: Delete task. Required: id\n- summary: Overview of all pending/in_progress tasks grouped by priority\n- set_preference: Save user preference. Required: key, value. Example: {\"action\":\"set_preference\",\"key\":\"morning_briefing\",\"value\":{\"enabled\":true,\"time\":\"08:00\"}}\n\nExamples:\n{\"action\":\"create\",\"title\":\"Call dentist\",\"due_date\":\"2026-03-10T10:00:00+01:00\",\"priority\":\"high\"}\n{\"action\":\"list\",\"status\":\"pending\"}\n{\"action\":\"update\",\"id\":5,\"status\":\"done\"}\n{\"action\":\"summary\"}",
"jsCode": "const SUPABASE_URL = '{{SUPABASE_URL}}';\nconst SUPABASE_KEY = '{{SUPABASE_SERVICE_KEY}}';\nasync function pgrest(method, path, body, extraHeaders = {}) {\n const opts = {\n method,\n url: `${SUPABASE_URL}${path}`,\n headers: { 'apikey': SUPABASE_KEY, 'Content-Type': 'application/json', ...extraHeaders },\n returnFullResponse: true,\n ignoreHttpStatusErrors: true\n };\n if (body) opts.body = JSON.stringify(body);\n const res = await helpers.httpRequest(opts);\n if (!res.body || res.body === '') return null;\n return typeof res.body === 'string' ? JSON.parse(res.body) : res.body;\n}\n\nlet input;\ntry { input = JSON.parse(query); } catch(e) { return 'Error: invalid JSON. Expected: {\"action\":\"list|create|update|delete|summary|set_preference\", ...}'; }\n\nconst userEmail = $('Merge Input').first().json.userEmail || 'unknown';\nconst userId = `oi:${userEmail}`;\nconst action = input.action;\n\nif (action === 'list') {\n let filter = `user_id=eq.${encodeURIComponent(userId)}`;\n if (input.status) filter += `&status=eq.${input.status}`;\n if (input.priority) filter += `&priority=eq.${input.priority}`;\n if (input.due_before) filter += `&due_date=lte.${input.due_before}`;\n filter += '&parent_id=is.null&order=due_date.asc.nullslast,priority.desc,created_at.desc';\n const tasks = await pgrest('GET', `/rest/v1/tasks?${filter}&select=*`);\n return JSON.stringify(tasks);\n\n} else if (action === 'create') {\n if (!input.title) return 'Error: title is required';\n const body = {\n user_id: userId,\n title: input.title,\n description: input.description || null,\n priority: input.priority || 'medium',\n due_date: input.due_date || null,\n parent_id: input.parent_id || null,\n tags: input.tags || []\n };\n const result = await pgrest('POST', '/rest/v1/tasks', body, { 'Prefer': 'return=representation' });\n return JSON.stringify({ success: true, task: Array.isArray(result) ? result[0] : result });\n\n} else if (action === 'update') {\n if (!input.id) return 'Error: id is required';\n const body = {};\n if (input.title) body.title = input.title;\n if (input.description !== undefined) body.description = input.description;\n if (input.status) {\n body.status = input.status;\n if (input.status === 'done') body.completed_at = new Date().toISOString();\n }\n if (input.priority) body.priority = input.priority;\n if (input.due_date !== undefined) body.due_date = input.due_date;\n if (input.tags) body.tags = input.tags;\n body.updated_at = new Date().toISOString();\n const result = await pgrest('PATCH', `/rest/v1/tasks?id=eq.${input.id}&user_id=eq.${encodeURIComponent(userId)}`, body, { 'Prefer': 'return=representation' });\n return JSON.stringify({ success: true, task: Array.isArray(result) ? result[0] : result });\n\n} else if (action === 'delete') {\n if (!input.id) return 'Error: id is required';\n await pgrest('DELETE', `/rest/v1/tasks?id=eq.${input.id}&user_id=eq.${encodeURIComponent(userId)}`);\n return JSON.stringify({ success: true, deleted: input.id });\n\n} else if (action === 'summary') {\n const tasks = await pgrest('GET', `/rest/v1/tasks?user_id=eq.${encodeURIComponent(userId)}&status=in.(pending,in_progress)&parent_id=is.null&order=priority.desc,due_date.asc.nullslast&select=*`);\n const profiles = await pgrest('GET', `/rest/v1/user_profiles?user_id=eq.${encodeURIComponent(userId)}&select=timezone`);\n const tz = (profiles && profiles[0] && profiles[0].timezone) || 'Europe/Berlin';\n const now = new Date();\n const todayStr = now.toLocaleDateString('en-CA', { timeZone: tz });\n const overdue = tasks.filter(t => { if (!t.due_date) return false; const d = new Date(t.due_date); return d < now && d.toLocaleDateString('en-CA', { timeZone: tz }) !== todayStr; });\n const dueToday = tasks.filter(t => { if (!t.due_date) return false; return new Date(t.due_date).toLocaleDateString('en-CA', { timeZone: tz }) === todayStr; });\n return JSON.stringify({\n total_pending: tasks.length, overdue: overdue.length, due_today: dueToday.length,\n by_priority: { urgent: tasks.filter(t => t.priority === 'urgent').length, high: tasks.filter(t => t.priority === 'high').length, medium: tasks.filter(t => t.priority === 'medium').length, low: tasks.filter(t => t.priority === 'low').length },\n tasks: tasks.map(t => ({ id: t.id, title: t.title, status: t.status, priority: t.priority, due_date: t.due_date }))\n });\n\n} else if (action === 'set_preference') {\n if (!input.key || input.value === undefined) return 'Error: key and value required';\n const profiles = await pgrest('GET', `/rest/v1/user_profiles?user_id=eq.${encodeURIComponent(userId)}&select=preferences`);\n const current = (profiles[0] && profiles[0].preferences) || {};\n current[input.key] = input.value;\n await pgrest('PATCH', `/rest/v1/user_profiles?user_id=eq.${encodeURIComponent(userId)}`, { preferences: current, updated_at: new Date().toISOString() }, { 'Prefer': 'return=minimal' });\n if (input.key === 'morning_briefing' && input.value.enabled !== undefined) {\n await pgrest('PATCH', `/rest/v1/heartbeat_config?check_name=eq.morning_briefing`, { enabled: input.value.enabled }, { 'Prefer': 'return=minimal' });\n }\n return JSON.stringify({ success: true, preference: input.key, value: input.value });\n\n} else {\n return 'Error: unknown action \"' + action + '\". Use: list, create, update, delete, summary, set_preference';\n}"
},
"id": "tool-task-manager",
"name": "Task Manager",
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.2,
"position": [
3072,
208
]
},
{
"parameters": {
"description": "Manage the MCP template library. Install, list, or remove MCP server templates from the catalog. Add API credentials for installed templates.\n\nActions:\n- list_templates: Show available templates. Optional: category filter.\n- install_template: Install a template by ID. Creates the MCP server workflow.\n- remove_template: Remove an installed template by ID.\n- add_credential: Generate secure one-time link(s) to add/update API key(s) for an installed template. Required: id. Optional: cred_key (if omitted, generates links for ALL required credentials).\n\nInput: JSON with action and parameters.\nExamples:\n{\"action\":\"list_templates\"}\n{\"action\":\"install_template\",\"id\":\"weather-openmeteo\"}\n{\"action\":\"remove_template\",\"id\":\"weather-openmeteo\"}\n{\"action\":\"add_credential\",\"id\":\"google-reviews\"}",
"workflowId": {
"__rl": true,
"value": "REPLACE_LIBRARY_MANAGER_ID",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"id": "tool-library-manager",
"name": "Library Manager",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
3200,
208
]
},
{
"parameters": {
"name": "expert_agent",
"description": "Delegate a task to a specialized expert agent. The expert works independently with its own AI model and tools, then returns a structured result. You should rephrase the result in your own tone before replying to the user.\n\nParameters (JSON):\n- agent: Expert agent identifier (e.g. \"research-expert\", \"content-creator\", \"data-analyst\")\n- task: Detailed task description \u2014 be specific about what you need\n- context: Relevant conversation context the expert should know (optional)\n\nExamples:\n{\"agent\":\"research-expert\",\"task\":\"Recherchiere die besten Wanderwege in Tirol mit Schwierigkeitsgrad und L\u00e4nge\",\"context\":\"User plant einen Wanderurlaub im Sommer\"}\n{\"agent\":\"content-creator\",\"task\":\"Erstelle einen Instagram-Post \u00fcber unser neues Produkt\",\"context\":\"Produkt: AI-gest\u00fctzter Kalender, Zielgruppe: junge Berufst\u00e4tige\"}\n{\"agent\":\"data-analyst\",\"task\":\"Analysiere diese Verkaufszahlen und erstelle einen Report\",\"context\":\"Q1 2026: Jan 45k, Feb 52k, M\u00e4r 61k\"}",
"workflowId": {
"__rl": true,
"value": "REPLACE_SUB_AGENT_RUNNER_ID",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"id": "tool-expert-agent",
"name": "Expert Agent",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
3328,
208
]
},
{
"parameters": {
"name": "agent_library",
"description": "Manage the expert agent library. Install, list, or remove specialized expert agents from the catalog.\n\nActions:\n- list_agents: Show all available agents from the catalog (with installed status).\n- install_agent: Install an agent by ID. Downloads persona from catalog and saves to database.\n- remove_agent: Remove an installed agent by ID.\n- list_installed: Show only currently installed agents.\n\nInput: JSON with action and parameters.\nExamples:\n{\"action\":\"list_agents\"}\n{\"action\":\"install_agent\",\"id\":\"research-expert\"}\n{\"action\":\"remove_agent\",\"id\":\"research-expert\"}\n{\"action\":\"list_installed\"}",
"workflowId": {
"__rl": true,
"value": "REPLACE_AGENT_LIBRARY_MANAGER_ID",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"id": "tool-agent-library",
"name": "Agent Library",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
3456,
208
]
},
{
"parameters": {
"description": "Read a webpage and return its content as clean markdown. Input: URL string or JSON with url field. Use this instead of HTTP Tool when you need to read webpage content (articles, docs, blog posts). Returns clean text without HTML boilerplate.",
"jsCode": "let input;\ntry { input = JSON.parse(query); } catch(e) { input = { url: query }; }\nif (!input.url) return 'Fehler: url ist Pflicht';\ntry {\n const resp = await helpers.httpRequest({\n method: 'POST',\n url: 'http://crawl4ai:11235/crawl',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ urls: [input.url], crawler_config: { type: 'CrawlerRunConfig', params: { stream: false, cache_mode: 'bypass' } } }),\n timeout: 30000\n });\n const results = resp.results || resp;\n const result = Array.isArray(results) ? results[0] : results;\n if (!result.success) return 'Fehler: ' + (result.error_message || 'Crawl fehlgeschlagen');\n const mdObj = result.markdown || {};\n const md = typeof mdObj === 'string' ? mdObj : (mdObj.raw_markdown || '');\n return md.substring(0, 8000);\n} catch(e) {\n return 'Fehler beim Abrufen: ' + e.message;\n}"
},
"id": "tool-web-reader",
"name": "Web Reader",
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.2,
"position": [
3584,
208
]
},
{
"parameters": {
"description": "Manage projects \u2014 persistent markdown documents for tracking ongoing work across conversations. Use this when a topic spans multiple sessions (e.g. building a presentation, planning an event, setting up a server).\n\nInput: JSON with action and fields:\n- list: Returns all projects with name, status, updated_at.\n- read: name (string). Returns the full markdown content.\n- create: name (string), content (string, markdown). Creates a new project.\n- update: name (string), content (string, markdown). Replaces the entire project content. Always read first, then send the complete updated document.\n- archive: name (string). Sets status to completed.\n\nExamples:\n{\"action\":\"list\"}\n{\"action\":\"create\",\"name\":\"KI Pr\u00e4sentation\",\"content\":\"## Ziel\\nPr\u00e4sentation \u00fcber KI im Tourismus\\n\\n## Gliederung\\n1. Einf\u00fchrung\\n2. Use Cases\"}\n{\"action\":\"read\",\"name\":\"KI Pr\u00e4sentation\"}\n{\"action\":\"update\",\"name\":\"KI Pr\u00e4sentation\",\"content\":\"## Ziel\\n...updated content...\"}\n{\"action\":\"archive\",\"name\":\"KI Pr\u00e4sentation\"}",
"jsCode": "const SUPABASE_URL = '{{SUPABASE_URL}}';\nconst SUPABASE_KEY = '{{SUPABASE_SERVICE_KEY}}';\n\nasync function pgrest(method, path, body, extraHeaders = {}) {\n const opts = {\n method,\n url: `${SUPABASE_URL}${path}`,\n headers: { 'apikey': SUPABASE_KEY, 'Content-Type': 'application/json', ...extraHeaders },\n returnFullResponse: true,\n ignoreHttpStatusErrors: true\n };\n if (body) opts.body = JSON.stringify(body);\n const res = await helpers.httpRequest(opts);\n if (!res.body || res.body === '') return null;\n return typeof res.body === 'string' ? JSON.parse(res.body) : res.body;\n}\n\nlet input;\ntry { input = JSON.parse(query); } catch(e) { return 'Error: invalid JSON. Expected: {\"action\":\"list|read|create|update|archive\", ...}'; }\n\nconst action = input.action;\n\nif (action === 'list') {\n const projects = await pgrest('GET', '/rest/v1/projects?order=updated_at.desc&select=name,status,updated_at');\n if (!projects || projects.length === 0) return JSON.stringify({ projects: [], message: 'No projects yet.' });\n return JSON.stringify({ projects });\n\n} else if (action === 'read') {\n if (!input.name) return 'Error: name is required';\n const projects = await pgrest('GET', `/rest/v1/projects?name=eq.${encodeURIComponent(input.name)}&select=name,status,content,created_at,updated_at`);\n if (!projects || projects.length === 0) return JSON.stringify({ error: 'Project not found: ' + input.name });\n return JSON.stringify(projects[0]);\n\n} else if (action === 'create') {\n if (!input.name) return 'Error: name is required';\n const body = { name: input.name, content: input.content || '', status: 'active' };\n const result = await pgrest('POST', '/rest/v1/projects', body, { 'Prefer': 'return=representation' });\n if (result && result.code) return JSON.stringify({ error: result.message || 'Create failed' });\n return JSON.stringify({ success: true, project: Array.isArray(result) ? result[0] : result });\n\n} else if (action === 'update') {\n if (!input.name) return 'Error: name is required';\n if (input.content === undefined) return 'Error: content is required';\n const body = { content: input.content, updated_at: new Date().toISOString() };\n if (input.status) body.status = input.status;\n const result = await pgrest('PATCH', `/rest/v1/projects?name=eq.${encodeURIComponent(input.name)}`, body, { 'Prefer': 'return=representation' });\n if (!result || result.length === 0) return JSON.stringify({ error: 'Project not found: ' + input.name });\n return JSON.stringify({ success: true, project: Array.isArray(result) ? result[0] : result });\n\n} else if (action === 'archive') {\n if (!input.name) return 'Error: name is required';\n const body = { status: 'completed', updated_at: new Date().toISOString() };\n const result = await pgrest('PATCH', `/rest/v1/projects?name=eq.${encodeURIComponent(input.name)}`, body, { 'Prefer': 'return=representation' });\n if (!result || result.length === 0) return JSON.stringify({ error: 'Project not found: ' + input.name });\n return JSON.stringify({ success: true, archived: input.name });\n\n} else {\n return 'Error: unknown action \"' + action + '\". Use: list, read, create, update, archive';\n}"
},
"id": "tool-project-manager",
"name": "Project Manager",
"type": "@n8n/n8n-nodes-langchain.toolCode",
"typeVersion": 1.2,
"position": [
3712,
208
]
},
{
"parameters": {
"description": "Manage the knowledge graph \u2014 entities (people, companies, projects, places, events, topics) and their relationships. Entities can be personal (scope='user', default) or organization-shared (scope='org'). Every search automatically returns both your own entities AND org-shared ones.\n\nInput: JSON with action and parameters.\n\nActions:\n- search: Find entities. Optional: query (semantic search), name (text match), type (free text filter, e.g. person/company/project)\n- save: Create entity. Required: name. Optional: entity_type (ALWAYS English lowercase \u2014 person, company, project, place, event, topic, product, skill, concept, or anything fitting), summary, metadata, scope ('user' DEFAULT = personal to you, 'org' = shared with the whole team \u2014 use for colleagues, member businesses, DMO facts, regional info)\n- update: Update entity (only if owned by user or org-shared). Required: id (UUID). Optional: name, entity_type, summary, metadata, scope (to promote user\u2192org or demote org\u2192user)\n- relate: Create relationship. Required: source_name, target_name, relation_type (ALWAYS English lowercase \u2014 works_at, knows, interested_in, recommended, collaborated_on, lives_in, manages, founded, etc.). Optional: weight (0-1), valid_from, valid_until\n- graph: Get entity network. Required: name. Optional: depth (default 2)\n- delete: Remove entity (only if owned by user or org-shared) and its relations. Required: id (UUID)\n\nSCOPE RULE: Default to 'user'. Use scope='org' when the entity is clearly team knowledge: colleagues (DMO staff), member businesses, regional facts, destination events, shared projects. If in doubt \u2192 user.\n\nExamples:\n{\"action\":\"save\",\"name\":\"Sandra Huber\",\"entity_type\":\"person\",\"summary\":\"Marketing-Lead Zugspitzregion\",\"scope\":\"org\"}\n{\"action\":\"save\",\"name\":\"Mein Zahnarzt Dr. Weber\",\"entity_type\":\"person\",\"scope\":\"user\"}\n{\"action\":\"relate\",\"source_name\":\"Sandra Huber\",\"target_name\":\"Zugspitzregion\",\"relation_type\":\"works_at\"}\n{\"action\":\"graph\",\"name\":\"Zugspitzregion\",\"depth\":2}\n{\"action\":\"search\",\"query\":\"tourism marketing\",\"type\":\"person\"}",
"jsCode": "const SUPABASE_URL = '{{SUPABASE_URL}}';\nconst SUPABASE_KEY = '{{SUPABASE_SERVICE_KEY}}';\n\n// Multi-user scope: sessionId from Build System Prompt\nconst sessionId = $('Build System Prompt').first().json.sessionId || '';\nif (!sessionId) return JSON.stringify({ error: 'sessionId missing from context \\u2014 cannot enforce user scope.' });\nconst scopeFilter = `or=(user_id.is.null,user_id.eq.${encodeURIComponent(sessionId)})`;\n\n// Helper: resolve scope string to user_id value ('user' -> sessionId, 'org' -> null)\nfunction resolveUserId(scope) {\n const s = (scope || 'user').toLowerCase();\n return s === 'org' ? null : sessionId;\n}\n\nasync function pgrest(method, path, body, extraHeaders = {}) {\n const opts = {\n method,\n url: `${SUPABASE_URL}${path}`,\n headers: { 'apikey': SUPABASE_KEY, 'Content-Type': 'application/json', ...extraHeaders },\n returnFullResponse: true,\n ignoreHttpStatusErrors: true\n };\n if (body) opts.body = JSON.stringify(body);\n const res = await helpers.httpRequest(opts);\n if (!res.body || res.body === '') return null;\n return typeof res.body === 'string' ? JSON.parse(res.body) : res.body;\n}\n\n// Normalize type strings: lowercase, spaces/hyphens to underscores, trim\nfunction normalize(s) {\n if (!s) return null;\n return s.trim().toLowerCase().replace(/\\u00e4/g,'ae').replace(/\\u00f6/g,'oe').replace(/\\u00fc/g,'ue').replace(/\\u00df/g,'ss').replace(/[\\s-]+/g, '_').replace(/[^a-z0-9_]/g, '');\n}\n\nasync function getEmbedding(text) {\n const cfgResp = await pgrest('GET', '/rest/v1/tools_config?tool_name=eq.embedding&select=config,enabled');\n const cfg = (Array.isArray(cfgResp) && cfgResp.length > 0 && cfgResp[0].enabled) ? cfgResp[0].config : null;\n if (!cfg || !cfg.api_key) return null;\n const provider = cfg.provider || 'openai';\n const apiKey = cfg.api_key;\n const model = cfg.model || 'text-embedding-3-small';\n try {\n if (provider === 'openai') {\n const res = await helpers.httpRequest({ method: 'POST', url: 'https://api.openai.com/v1/embeddings', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model, input: text }) });\n return res.data[0].embedding;\n }\n if (provider === 'voyage') {\n const res = await helpers.httpRequest({ method: 'POST', url: 'https://api.voyageai.com/v1/embeddings', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model || 'voyage-3-lite', input: [text] }) });\n return res.data[0].embedding;\n }\n if (provider === 'ollama') {\n const ollamaUrl = cfg.ollama_url || 'http://localhost:11434';\n const res = await helpers.httpRequest({ method: 'POST', url: `${ollamaUrl}/api/embed`, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model || 'nomic-embed-text', input: text }) });\n return
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.
anthropicApihttpHeaderAuthpostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
🤖 DMO Claw. Uses executeWorkflowTrigger, postgres, agent, lmChatAnthropic. Webhook trigger; 37 nodes.
Source: https://github.com/freddy-schuetz/dmo-claw/blob/26e19d91f6d134dc61461e9059855ef6e48da228/workflows/dmo-claw.json — 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.
Integrates GHL + Wazzap with Redis and an AI Agent using ClientInfo to process messages, generate accurate replies, and send them via a custom field trigger.
Ask your PostgreSQL database complex questions and receive clear summaries, charts, and even update or insert data — all through one smart agent powered by n8n’s Model Context Protocol (MCP).
Enable AI-driven conversations with your PostgreSQL database using a secure and visual-free agent powered by n8n’s Model Context Protocol (MCP). This template allows users to ask multiple KPIs in a si
Local AI Expert. Uses lmChatAnthropic, memoryPostgresChat, executeWorkflowTrigger, supabase. Webhook trigger; 15 nodes.
CLINICAINTEGRAL_secretary. Uses postgres, mcpClientTool, googleDriveTool, toolWorkflow. Webhook trigger; 89 nodes.