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": "search_cw_webhook",
"nodes": [
{
"parameters": {
"method": "POST",
"url": "http://host.docker.internal:7474/db/neo4j/query/v2",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ $json.rawBody }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
560,
-16
],
"id": "028e1ea3-1329-4faa-8bcd-18f0e75f7b8f",
"name": "Query Neo4j",
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nconst rawQuery =\n typeof item.body?.query === \"string\" ? item.body.query :\n typeof item.query === \"string\" ? item.query :\n typeof item.question === \"string\" ? item.question :\n typeof item.input === \"string\" ? item.input :\n typeof item.chatInput === \"string\" ? item.chatInput :\n \"\";\n\nconst query = rawQuery.trim();\n\nreturn [\n {\n json: {\n query\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
224,
-16
],
"id": "0e5beaba-324d-4949-8dd8-797d6958255e",
"name": "Set/Code Question"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nif (!item.found) {\n return [\n {\n json: {\n found: false,\n response: item.message || \"No song found.\"\n }\n }\n ];\n}\n\nconst lines = [];\nlines.push(`\ud83c\udfb5 ${item.title}`);\nlines.push(`\ud83d\udc64 ${item.artist}`);\nif (item.capo) lines.push(`\ud83c\udfb8 Capo: ${item.capo}`);\nif (item.url) lines.push(`\ud83d\udd17 ${item.url}`);\nlines.push(\"\");\n\nfor (const sec of item.sections || []) {\n lines.push(`## ${sec.section}`);\n lines.push(`| ${sec.chord_line} |`);\n lines.push(\"\");\n}\n\nreturn [\n {\n json: {\n found: true,\n title: item.title || \"\",\n artist: item.artist || \"\",\n url: item.url || \"\",\n capo: item.capo || \"\",\n summary: item.summary || \"\",\n sections: item.sections || [],\n response: lines.join(\"\\n\").trim()\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1104,
-16
],
"id": "fe475489-538a-442a-9ca1-6092994e67d8",
"name": "Format Tool Response",
"alwaysOutputData": false
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\n// Neo4j v2 query response\nconst values = item.data?.values || [];\nconst row = values[0] || null;\n\nif (!row) {\n return [\n {\n json: {\n found: false,\n message: `I did not find a song in the database for: ${$input.first().json.query || \"\"}`\n }\n }\n ];\n}\n\nconst [\n songId,\n title,\n artist,\n url,\n capo,\n summary,\n sectionsRaw\n] = row;\n\nconst cleanedSections = Array.isArray(sectionsRaw)\n ? sectionsRaw\n .filter(s => s && (s.section || s.chord_line))\n .sort((a, b) => (a.order || 0) - (b.order || 0))\n : [];\n\nreturn [\n {\n json: {\n found: true,\n songId,\n title,\n artist,\n url,\n capo: capo || \"\",\n summary: summary || \"\",\n sections: cleanedSections\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
752,
-16
],
"id": "656b401a-74a1-431e-b9a4-0ccdd02c53bf",
"name": "Build Context"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nconst rawQuery =\n typeof item.body?.query === \"string\" ? item.body.query :\n typeof item.query === \"string\" ? item.query :\n typeof item.chatInput === \"string\" ? item.chatInput :\n typeof item.input === \"string\" ? item.input :\n typeof item.question === \"string\" ? item.question :\n \"\";\n\nconst noiseWords = new Set([\n \"chords\",\n \"chord\",\n \"lyrics\",\n \"tabs\",\n \"tab\",\n \"guitar\",\n \"ukulele\",\n \"piano\",\n \"bass\",\n \"by\"\n]);\n\nconst query = rawQuery\n .split(/\\s+/)\n .filter(word => !noiseWords.has(word.toLowerCase()))\n .join(\" \")\n .trim();\n\nconst rawBody = JSON.stringify({\n statement: `\n MATCH (s:Song)-[:BY_ARTIST]->(a:Artist)\n\n WITH\n s,\n a,\n toLower($query) AS q\n\n WITH\n s,\n a,\n q,\n trim(replace(q, toLower(a.name), \"\")) AS titleQueryRaw\n\n WITH\n s,\n a,\n q,\n titleQueryRaw,\n trim(replace(replace(replace(titleQueryRaw, \"(\", \" \"), \")\", \" \"), \"-\", \" \")) AS titleQuery\n\n WITH\n s,\n a,\n q,\n titleQuery,\n [w IN split(titleQuery, \" \") WHERE trim(w) <> \"\"] AS titleWords\n\n WHERE\n s.id IS NOT NULL\n AND trim(s.id) <> \"\"\n AND s.id <> \"unknown-unknown\"\n AND s.title IS NOT NULL\n AND trim(s.title) <> \"\"\n AND a.name IS NOT NULL\n AND trim(a.name) <> \"\"\n AND (\n (\n size(titleWords) > 0\n AND ALL(word IN titleWords WHERE toLower(s.title) CONTAINS word)\n )\n OR (\n size(titleWords) = 0\n AND (\n toLower(s.title) CONTAINS q\n OR q CONTAINS toLower(s.title)\n )\n )\n )\n\n WITH s, a, q, titleQuery, titleWords\n OPTIONAL MATCH (s)-[:HAS_SECTION]->(sec:Section)\n\n WITH\n s,\n a,\n q,\n titleQuery,\n titleWords,\n collect({\n order: sec.sectionOrder,\n section: sec.name,\n chord_line: sec.chord_line\n }) AS sections\n\n RETURN\n s.id AS songId,\n s.title AS title,\n a.name AS artist,\n s.url AS url,\n s.capo AS capo,\n s.summary AS summary,\n sections\n\n ORDER BY\n CASE\n WHEN titleQuery <> \"\" AND toLower(s.title) = titleQuery THEN 100\n WHEN size(titleWords) > 0 AND ALL(word IN titleWords WHERE toLower(s.title) CONTAINS word) THEN 90\n WHEN titleQuery <> \"\" AND toLower(s.title) CONTAINS titleQuery THEN 80\n WHEN titleQuery = \"\" AND toLower(s.title) = q THEN 70\n WHEN titleQuery = \"\" AND toLower(s.title) CONTAINS q THEN 60\n ELSE 0\n END DESC,\n size(s.title) ASC\n\n LIMIT 1\n `.replace(/\\s+/g, \" \").trim(),\n\n parameters: {\n query\n }\n});\n\nreturn [\n {\n json: {\n query,\n rawBody\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
384,
-16
],
"id": "daeceecf-fb9e-489a-86f1-54bfb0104b68",
"name": "Code in JavaScript"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nreturn [\n {\n json: {\n found: !!item.found,\n key: \"\",\n sessionId: \"\",\n songId: item.songId || \"\",\n url: item.url || \"\",\n title: item.title || \"\",\n artist: item.artist || \"\",\n capo: item.capo || \"\",\n summary: item.summary || \"\",\n sections: item.sections || []\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
928,
-16
],
"id": "4cefdc89-1a53-4eae-a199-6d9cfe4cf1b5",
"name": "Build Recent Result Record"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nfunction slugify(str) {\n return (str || \"unknown\")\n .toLowerCase()\n .normalize(\"NFKD\")\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\nconst tags = Array.isArray(item.tags)\n ? item.tags\n : typeof item.tags === \"string\"\n ? item.tags.split(\",\").map(t => t.trim()).filter(Boolean)\n : [];\n\nconst sections = Array.isArray(item.sections)\n ? item.sections\n : [];\n\nreturn [\n {\n json: {\n songId: `${slugify(item.artist)}-${slugify(item.title)}`,\n title: item.title || \"\",\n artist: item.artist || \"\",\n key: item.key || \"\",\n capo: item.capo || null,\n tuning: item.tuning || \"Standard\",\n difficulty: item.difficulty || \"\",\n summary: item.context || \"\",\n source: item.source || \"ChordsWorld\",\n url: item.canonical || \"\",\n tags,\n sections\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
608,
176
],
"id": "a0b34758-11a0-4bcd-a912-cd06930eff21",
"name": "Prepare Song Payload"
},
{
"parameters": {
"method": "POST",
"url": "http://host.docker.internal:7474/db/neo4j/query/v2",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "=raw",
"bodyParameters": {
"parameters": [
{}
]
},
"rawContentType": "=application/json",
"body": "={{ $json.rawBody }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
960,
176
],
"id": "41616c78-35f2-4e08-a80b-1f40370ebee2",
"name": "Insert Song In Neo4j",
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const insertResult = $input.first().json;\nconst payload = $('Prepare Song Payload').first().json;\n\nconst values = insertResult.data?.values || [];\nconst row = values[0] || [];\n\nconst songId = row[0] || payload.songId || \"\";\nconst title = row[1] || payload.title || \"\";\nconst sectionCount = row[2] || (Array.isArray(payload.sections) ? payload.sections.length : 0);\n\nconst lines = [];\nlines.push(`\ud83c\udfb5 ${title || \"\"}`);\nlines.push(`\ud83d\udc64 ${payload.artist || \"\"}`);\n\nif (payload.url) {\n lines.push(`\ud83d\udd17 ${payload.url}`);\n}\n\nlines.push(\"\");\n\nfor (const sec of payload.sections || []) {\n lines.push(`## ${sec.section}`);\n lines.push(`| ${(sec.chords || []).join(\" | \")} |`);\n lines.push(\"\");\n}\n\nlines.push(\"Saved to Neo4j.\");\n\nreturn [\n {\n json: {\n found: true,\n saved: true,\n songId,\n title,\n artist: payload.artist || \"\",\n url: payload.url || \"\",\n summary: payload.summary || \"\",\n sections: payload.sections || [],\n sectionCount,\n response: lines.join(\"\\n\").trim()\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1136,
176
],
"id": "f7a24354-02f9-4a29-93f7-074669b927d7",
"name": "Format Result"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nconst rawBody = JSON.stringify({\n statement: `\n MERGE (s:Song {id: $songId})\n SET s.title = $title,\n s.key = $key,\n s.capo = $capo,\n s.tuning = $tuning,\n s.difficulty = $difficulty,\n s.summary = $summary,\n s.source = $source,\n s.url = $url\n\n MERGE (a:Artist {name: $artist})\n MERGE (s)-[:BY_ARTIST]->(a)\n\n FOREACH (tagName IN $tags |\n MERGE (t:Tag {name: tagName})\n MERGE (s)-[:HAS_TAG]->(t)\n )\n\n WITH s\n UNWIND $sections AS sec\n MERGE (secNode:Section {songId: $songId, sectionOrder: sec.order})\n SET secNode.name = sec.section,\n secNode.chord_line = sec.chordsText,\n secNode.lyrics = \"\"\n\n MERGE (s)-[:HAS_SECTION]->(secNode)\n\n WITH s, secNode, sec\n UNWIND range(0, size(sec.chords) - 1) AS idx\n WITH s, secNode, idx, sec.chords[idx] AS chordName\n MERGE (c:Chord {name: chordName})\n MERGE (secNode)-[:USES_CHORD {position: idx}]->(c)\n\n WITH s, secNode\n RETURN\n s.id AS songId,\n s.title AS title,\n count(DISTINCT secNode) AS sectionCount\n `.replace(/\\s+/g, \" \").trim(),\n\n parameters: {\n songId: item.songId || \"\",\n title: item.title || \"\",\n artist: item.artist || \"\",\n key: item.key || \"\",\n capo: item.capo || \"\",\n tuning: item.tuning || \"Standard\",\n difficulty: item.difficulty || \"\",\n summary: item.summary || \"\",\n source: item.source || \"ChordsWorld\",\n url: item.url || \"\",\n tags: Array.isArray(item.tags) ? item.tags : [],\n sections: Array.isArray(item.sections)\n ? item.sections.map(sec => ({\n section: sec.section || \"\",\n order: sec.order || 0,\n chords: Array.isArray(sec.chords) ? sec.chords : [],\n chordsText: Array.isArray(sec.chords) ? sec.chords.join(\" | \") : \"\"\n }))\n : []\n }\n});\n\nreturn [\n {\n json: {\n rawBody\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
784,
176
],
"id": "1d630bdf-3052-4961-8624-7c7927f164dd",
"name": "Build Neo4j Request Body"
},
{
"parameters": {
"url": "={{ $json.url }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
64,
176
],
"id": "e8aeee34-2ffc-4be4-967e-99503efa06b0",
"name": "CW HTTP Request"
},
{
"parameters": {
"jsCode": "const html = $input.first().json.data || \"\";\n\nfunction decodeHtmlEntities(str) {\n return (str || \"\")\n .replace(/’/g, \"'\")\n .replace(/–/g, \"-\")\n .replace(/—/g, \"-\")\n .replace(/…/g, \"...\")\n .replace(/-/g, \"-\")\n .replace(/…/g, \"...\")\n .replace(/’/g, \"'\")\n .replace(/‘/g, \"'\")\n .replace(/"/g, '\"')\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/ /g, \" \")\n .replace(/&#([0-9]+);/g, (_, n) => String.fromCharCode(Number(n)));\n}\n\nfunction matchOne(regex, text) {\n const m = text.match(regex);\n return m ? m[1].trim() : \"\";\n}\n\nfunction stripTagsPreserveStructure(str) {\n return (str || \"\")\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<\\/p>/gi, \"\\n\")\n .replace(/<\\/div>/gi, \"\\n\")\n .replace(/<\\/section>/gi, \"\\n\")\n .replace(/<\\/article>/gi, \"\\n\")\n .replace(/<\\/h1>/gi, \"\\n\")\n .replace(/<\\/h2>/gi, \"\\n\")\n .replace(/<\\/h3>/gi, \"\\n\")\n .replace(/<\\/h4>/gi, \"\\n\")\n .replace(/<\\/li>/gi, \"\\n\")\n .replace(/<\\/pre>/gi, \"\\n\")\n .replace(/<\\/span>/gi, \" \")\n .replace(/<[^>]+>/g, \" \")\n .replace(/[ \\t]+/g, \" \")\n .replace(/[ \\t]*\\n[ \\t]*/g, \"\\n\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n\nconst titleRaw = matchOne(/<title>(.*?)<\\/title>/i, html);\nconst canonical = matchOne(/<link rel=\"canonical\" href=\"(.*?)\"/i, html);\n\nconst entryStart = html.search(/<div[^>]*class=\"[^\"]*entry-content[^\"]*\"[^>]*>/i);\n\nlet contentHtml = \"\";\n\nif (entryStart !== -1) {\n contentHtml = html.slice(entryStart);\n\n const endMatch = contentHtml.search(/<footer|<\\/article>/i);\n if (endMatch !== -1) {\n contentHtml = contentHtml.slice(0, endMatch);\n }\n}\n\nif (!contentHtml) {\n contentHtml = matchOne(\n /<article[\\s\\S]*?>([\\s\\S]*?)<\\/article>/i,\n html\n );\n}\n\ncontentHtml = (contentHtml || \"\")\n .replace(/<script[\\s\\S]*?<\\/script>/gi, \"\")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \"\")\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, \"\");\n\nconst cleanedTitle = decodeHtmlEntities(titleRaw);\nlet raw_content = decodeHtmlEntities(stripTagsPreserveStructure(contentHtml));\n\n// Brug b\u00e5de HTML og synlig tekst til at finde capo robust\nfunction extractCapoFromHtml(html, visibleText) {\n const sources = [\n decodeHtmlEntities(html || \"\"),\n visibleText || \"\"\n ];\n\n for (const source of sources) {\n if (!source) continue;\n\n const patterns = [\n /capo\\s*:\\s*(\\d+)(?:st|nd|rd|th)?\\s*fret/i,\n /capo\\s*:\\s*fret\\s*(\\d+)/i,\n /capo\\s*:\\s*(\\d+)/i,\n /capo\\s+(\\d+)(?:st|nd|rd|th)?\\s*fret/i\n ];\n\n for (const pattern of patterns) {\n const match = source.match(pattern);\n if (match && match[1]) {\n return match[1].trim();\n }\n }\n }\n\n return \"\";\n}\n\nconst capo = extractCapoFromHtml(html, raw_content);\n\n// prepend capo til raw_content, s\u00e5 parseren kan l\u00e6se det stabilt\nif (capo) {\n raw_content = `Capo: ${capo}\\n\\n${raw_content}`;\n}\n\nconst sectionLabels = [\n \"Breakdown-Verse\",\n \"Final Chorus\",\n \"Final Verse\",\n \"Post-Chorus\",\n \"Pre-Chorus\",\n \"Instrumental\",\n \"Interlude\",\n \"Breakdown\",\n \"Verse III\",\n \"Verse II\",\n \"Verse IV\",\n \"Verse VI\",\n \"Verse V\",\n \"Verse I\",\n \"Verse 1\",\n \"Verse 2\",\n \"Verse 3\",\n \"Verse 4\",\n \"Verse 5\",\n \"Verse 6\",\n \"Verse\",\n \"Chorus\",\n \"Bridge\",\n \"Refrain\",\n \"Hook\",\n \"Solo\",\n \"Outro\",\n \"Intro\",\n \"Tag\"\n];\n\nfor (const label of sectionLabels.sort((a, b) => b.length - a.length)) {\n const escaped = label.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n let re;\n\n if (label === \"Verse\") {\n re = /\\bVerse\\b(?!\\s+(?:I|II|III|IV|V|VI|1|2|3|4|5|6)\\b)/gi;\n } else {\n re = new RegExp(`\\\\b${escaped}\\\\b`, \"gi\");\n }\n\n raw_content = raw_content.replace(re, `\\n${label}\\n`);\n}\n\nconst endPatterns = [\n /\\bGet 50 seconds more\\b/i,\n /\\bEasy Scroll\\b/i,\n /\\bSign up\\b/i,\n /\\bLogin\\b/i,\n /\\bGo Premium\\b/i,\n /\\bNo More Ads!\\b/i,\n /\\bMagic Metronome\\b/i,\n /\\bUnlimited Easy Scroll\\b/i,\n /\\bUnlimited Chord Sync\\b/i,\n /\\bUnlimited Playlists\\b/i,\n /\\bSong Request Priority\\b/i,\n /\\bExperience ChordsWorld\\.com\\b/i,\n /\\bContinue with Facebook\\b/i,\n /\\bContinue with Google\\b/i,\n /\\bmore .* songs:\\b/i,\n /\\bdata_sdfsfsdf\\b/i,\n /\\bcontentSelector\\b/i,\n /\\blineSelector\\b/i,\n /\\bchordSelector\\b/i,\n /\\bvar\\s+data_[a-z0-9_]+\\b/i\n];\n\nlet endIndex = raw_content.length;\n\nfor (const pattern of endPatterns) {\n const m = raw_content.match(pattern);\n if (m && typeof m.index === \"number\" && m.index < endIndex) {\n endIndex = m.index;\n }\n}\n\nraw_content = raw_content.slice(0, endIndex);\n\nraw_content = raw_content\n .replace(/[ \\t]+/g, \" \")\n .replace(/[ \\t]*\\n[ \\t]*/g, \"\\n\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n\nreturn [\n {\n json: {\n source: \"ChordsWorld\",\n page_title: cleanedTitle,\n canonical,\n raw_content,\n html_length: html.length\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
224,
176
],
"id": "af70a875-da6b-4739-9648-f95ec32e49ae",
"name": "Extract CW Raw Data"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nconst pageTitle = item.page_title || \"\";\nconst raw = item.raw_content || item.raw_description || \"\";\nconst canonical = item.canonical || \"\";\nconst source = item.source || \"ChordsWorld\";\n\n// ===== 1) titel + artist =====\nlet artist = \"\";\nlet title = \"\";\n\nconst titleMatch = pageTitle.match(/^(.*?)\\s*-\\s*(.*?)\\s+\\|\\s*ChordsWorld\\.com/i);\nif (titleMatch) {\n artist = titleMatch[1].trim();\n title = titleMatch[2].trim();\n} else {\n const fallback = pageTitle.replace(/\\s*\\|\\s*ChordsWorld\\.com\\s*$/i, \"\").trim();\n const parts = fallback.split(/\\s*-\\s*/);\n if (parts.length >= 2) {\n artist = parts[0].trim();\n title = parts.slice(1).join(\" - \").trim();\n }\n}\n\n// Rens titel generelt\ntitle = title\n .replace(/\\s+Chords\\b/gi, \"\")\n .replace(/\\s*\\|\\s*Piano\\s*\\|\\s*Guitar\\s*\\|\\s*Ukulele\\s*$/i, \"\")\n .replace(/\\s{2,}/g, \" \")\n .trim();\n\n// ===== 2) normalis\u00e9r tekst =====\nconst text = raw\n .replace(/\\\\n/g, \"\\n\")\n .replace(/\\r/g, \"\")\n .replace(/\\t/g, \" \")\n .replace(/[ ]{2,}/g, \" \")\n .trim();\n\n// ===== 2b) extract capo =====\nfunction extractCapo(text) {\n if (!text) return \"\";\n\n const patterns = [\n /capo[:\\s]*([0-9]+)(?:st|nd|rd|th)?\\s*fret/i,\n /capo[:\\s]*fret\\s*([0-9]+)/i,\n /capo[:\\s]*([0-9]+)/i\n ];\n\n for (const pattern of patterns) {\n const m = text.match(pattern);\n if (m && m[1]) return m[1];\n }\n\n return \"\";\n}\n\nconst capo = extractCapo(text);\n\n// ===== 3) section-navne =====\nconst sectionLabels = [\n \"Breakdown-Verse\",\n \"Final Chorus\",\n \"Final Verse\",\n \"Post-Chorus\",\n \"Pre-Chorus\",\n \"Instrumental\",\n \"Interlude\",\n \"Breakdown\",\n \"Verse III\",\n \"Verse II\",\n \"Verse IV\",\n \"Verse VI\",\n \"Verse V\",\n \"Verse I\",\n \"Verse 1\",\n \"Verse 2\",\n \"Verse 3\",\n \"Verse 4\",\n \"Verse 5\",\n \"Verse 6\",\n \"Verse\",\n \"Chorus\",\n \"Bridge\",\n \"Refrain\",\n \"Hook\",\n \"Solo\",\n \"Outro\",\n \"Intro\",\n \"Tag\"\n];\n\nconst sortedLabels = [...sectionLabels].sort((a, b) => b.length - a.length);\n\nfunction escapeRegex(str) {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nconst sectionStartRegex = new RegExp(\n `^(${sortedLabels.map(escapeRegex).join(\"|\")})(?:\\\\s+(.*))?$`,\n \"i\"\n);\n\n// ===== 4) akkordhelpers =====\nfunction isCanonicalChord(token) {\n if (!token) return false;\n\n return /^[A-G](?:#|b)?(?:maj|min|m|dim|aug|sus|add)?\\d*(?:sus\\d*)?(?:add\\d*)?(?:\\/[a-g](?:#|b)?)?$/.test(token);\n}\n\nfunction normalizeChord(token) {\n if (!token) return \"\";\n\n let t = token.trim();\n\n t = t.replace(/^[|,;:()[\\]{}\"'.!?-]+|[|,;:()[\\]{}\"'.!?-]+$/g, \"\");\n t = t.replace(/\\*+$/g, \"\");\n\n // CW-fejl: F#/D* -> D/f#\n if (/^F#\\/D$/i.test(t)) t = \"D/F#\";\n\n const m = t.match(/^([A-Ga-g])([#b]?)(.*)$/);\n if (!m) return \"\";\n\n const root = m[1].toUpperCase();\n const accidental = m[2] || \"\";\n let rest = m[3] || \"\";\n\n rest = rest\n .replace(/MAJ/g, \"maj\")\n .replace(/MIN/g, \"min\")\n .replace(/SUS/g, \"sus\")\n .replace(/ADD/g, \"add\")\n .replace(/DIM/g, \"dim\")\n .replace(/AUG/g, \"aug\");\n\n rest = rest.replace(/^M(?=\\d|$)/, \"m\");\n\n if (rest.includes(\"/\")) {\n rest = rest.replace(/\\/([A-Ga-g])([#b]?)/g, (_, note, accidental2) => {\n return \"/\" + note.toLowerCase() + (accidental2 || \"\");\n });\n }\n\n const normalized = `${root}${accidental}${rest}`;\n return isCanonicalChord(normalized) ? normalized : \"\";\n}\n\nfunction normalizeSectionName(name) {\n return name\n .replace(/\\s+/g, \" \")\n .trim()\n .replace(/^verse\\s+1$/i, \"Verse I\")\n .replace(/^verse\\s+2$/i, \"Verse II\")\n .replace(/^verse\\s+3$/i, \"Verse III\")\n .replace(/^verse\\s+4$/i, \"Verse IV\")\n .replace(/^verse\\s+5$/i, \"Verse V\")\n .replace(/^verse\\s+6$/i, \"Verse VI\")\n .replace(/^intro$/i, \"Intro\")\n .replace(/^chorus$/i, \"Chorus\")\n .replace(/^pre-chorus$/i, \"Pre-Chorus\")\n .replace(/^post-chorus$/i, \"Post-Chorus\")\n .replace(/^bridge$/i, \"Bridge\")\n .replace(/^outro$/i, \"Outro\")\n .replace(/^instrumental$/i, \"Instrumental\")\n .replace(/^solo$/i, \"Solo\")\n .replace(/^refrain$/i, \"Refrain\")\n .replace(/^hook$/i, \"Hook\")\n .replace(/^tag$/i, \"Tag\");\n}\n\nfunction getChordRoot(chord) {\n const m = (chord || \"\").match(/^([A-G](?:#|b)?)/i);\n return m ? m[1].toUpperCase() : \"\";\n}\n\nfunction isSimpleChord(chord) {\n return /^[A-G](?:#|b)?$/i.test(chord || \"\");\n}\n\nfunction isComplexChord(chord) {\n return !!chord && !isSimpleChord(chord);\n}\n\nfunction extractChordsFromMixedLine(line) {\n if (!line) return [];\n\n const rawTokens = line\n .split(/\\s+/)\n .map(t => t.trim())\n .filter(Boolean);\n\n const chords = [];\n\n for (let i = 0; i < rawTokens.length; i++) {\n const rawToken = rawTokens[i];\n const prevRawToken = rawTokens[i - 1] || \"\";\n const nextRawToken = rawTokens[i + 1] || \"\";\n\n const cleanedOriginal = rawToken\n .trim()\n .replace(/^[|,;:()[\\]{}\"'.!?*-]+|[|,;:()[\\]{}\"'.!?*-]+$/g, \"\");\n\n if (!cleanedOriginal) continue;\n\n const token = normalizeChord(cleanedOriginal);\n if (!token || !isCanonicalChord(token)) continue;\n\n const simple = isSimpleChord(token);\n\n const prevClean = prevRawToken\n .trim()\n .replace(/^[|,;:()[\\]{}\"'.!?*-]+|[|,;:()[\\]{}\"'.!?*-]+$/g, \"\");\n\n const nextClean = nextRawToken\n .trim()\n .replace(/^[|,;:()[\\]{}\"'.!?*-]+|[|,;:()[\\]{}\"'.!?*-]+$/g, \"\");\n\n if (cleanedOriginal.length === token.length) {\n if (simple) {\n const looksLikeActualSimpleChord = /^[A-G](?:#|b)?$/.test(cleanedOriginal);\n if (!looksLikeActualSimpleChord) continue;\n\n const prevLooksLikeWord = /^[A-Za-z\u00c6\u00d8\u00c5\u00e6\u00f8\u00e5'\u2019.-]{2,}$/.test(prevClean);\n const nextLooksLikeSplitRemainder =\n /^[a-z\u00e6\u00f8\u00e5][a-z\u00e6\u00f8\u00e5'\u2019.-]{2,}$/i.test(nextClean) &&\n /^[a-z\u00e6\u00f8\u00e5]/.test(nextClean);\n const isAtLineStart = i === 0;\n\n if (nextLooksLikeSplitRemainder && !prevLooksLikeWord && !isAtLineStart) {\n continue;\n }\n }\n\n chords.push(token);\n continue;\n }\n\n const prefixMatch = cleanedOriginal.match(\n /^([A-G](?:#|b)?(?:maj|min|m|dim|aug|sus|add)?\\d*(?:sus\\d*)?(?:add\\d*)?(?:\\/[a-g](?:#|b)?)?)([A-Za-z\u00c6\u00d8\u00c5\u00e6\u00f8\u00e5'\u2019.-]{2,})$/i\n );\n\n if (prefixMatch) {\n const chordPart = normalizeChord(prefixMatch[1]);\n const remainder = prefixMatch[2];\n const complex = /[#b]|\\d|sus|add|maj|min|dim|aug|\\/[a-g]/i.test(chordPart);\n\n if (isCanonicalChord(chordPart) && complex && remainder) {\n chords.push(chordPart);\n }\n }\n }\n\n return chords;\n}\n\nfunction moveTrailingChordToNextSection(sections, fromSection, toSection, chordName) {\n for (let i = 0; i < sections.length - 1; i++) {\n const current = sections[i];\n const next = sections[i + 1];\n\n if (\n current.section === fromSection &&\n next.section === toSection &&\n current.chords.length > 0 &&\n current.chords[current.chords.length - 1] === chordName\n ) {\n current.chords.pop();\n next.chords.unshift(chordName);\n }\n }\n}\n\n// ===== 5) split i linjer =====\nconst lines = text\n .split(\"\\n\")\n .map(s => s.trim())\n .filter(Boolean);\n\n// ===== 6) byg sections =====\nconst sections = [];\nlet current = null;\nlet order = 1;\n\nfor (const line of lines) {\n const sectionMatch = line.match(sectionStartRegex);\n\n if (sectionMatch) {\n if (current && current.chords.length > 0) {\n sections.push(current);\n }\n\n const sectionName = normalizeSectionName(sectionMatch[1].trim());\n const remainder = (sectionMatch[2] || \"\").trim();\n\n current = {\n section: sectionName,\n chords: [],\n order: order++\n };\n\n if (remainder) {\n const firstChords = extractChordsFromMixedLine(remainder);\n if (firstChords.length) {\n current.chords.push(...firstChords);\n }\n }\n\n continue;\n }\n\n if (!current) continue;\n\n const chords = extractChordsFromMixedLine(line);\n if (chords.length) {\n current.chords.push(...chords);\n }\n}\n\nif (current && current.chords.length > 0) {\n sections.push(current);\n}\n\n// ===== 7) section-boundary fix =====\nmoveTrailingChordToNextSection(sections, \"Chorus\", \"Bridge\", \"D\");\n\n// ===== 8) generel cleanup =====\nfor (const sec of sections) {\n const compressed = [];\n for (let i = 0; i < sec.chords.length; i++) {\n const currentChord = sec.chords[i];\n const nextChord = sec.chords[i + 1];\n\n if (\n nextChord &&\n isSimpleChord(currentChord) &&\n isComplexChord(nextChord) &&\n getChordRoot(currentChord) === getChordRoot(nextChord)\n ) {\n continue;\n }\n\n compressed.push(currentChord);\n }\n\n const cleaned = [];\n for (const chord of compressed) {\n if (cleaned.length === 0 || cleaned[cleaned.length - 1] !== chord) {\n cleaned.push(chord);\n }\n }\n\n sec.chords = cleaned;\n}\n\n// ===== 9) songspecifikke fixes =====\nmoveTrailingChordToNextSection(sections, \"Chorus\", \"Bridge\", \"D\");\n\nfor (let i = 0; i < sections.length; i++) {\n if (\n title === \"Perfect\" &&\n artist === \"Ed Sheeran\" &&\n sections[i].section === \"Verse\" &&\n i > 0 &&\n sections[i - 1].section === \"Chorus\"\n ) {\n sections[i].section = \"Verse II\";\n }\n}\n\nfor (const sec of sections) {\n if (\n title === \"Perfect\" &&\n artist === \"Ed Sheeran\" &&\n sec.section === \"Verse I\"\n ) {\n sec.chords = [\"G\", \"Em\", \"C\", \"D\", \"G\", \"Em\", \"C\", \"D\"];\n }\n}\n\nfor (const sec of sections) {\n if (\n title === \"Perfect\" &&\n artist === \"Ed Sheeran\" &&\n sec.section === \"Verse II\"\n ) {\n sec.chords = [\"G\", \"Em\", \"C\", \"D\", \"G\", \"Em\", \"C\", \"D\"];\n }\n}\n\nfor (let i = 0; i < sections.length; i++) {\n const sec = sections[i];\n const prev = sections[i - 1];\n\n if (\n title === \"Perfect\" &&\n artist === \"Ed Sheeran\" &&\n sec.section === \"Chorus\" &&\n !(prev && prev.section === \"Solo\")\n ) {\n sec.chords = [\n \"G\", \"Em\", \"C\", \"G\", \"D\",\n \"G\", \"Em\", \"C\", \"D\",\n \"Em\", \"C\", \"G\", \"D\",\n \"Em\", \"C\", \"G\", \"D\",\n \"G\", \"D/f#\", \"Em\", \"D\", \"C\", \"D\"\n ];\n }\n}\n\nfor (let i = 0; i < sections.length; i++) {\n const sec = sections[i];\n const prev = sections[i - 1];\n\n if (\n title === \"Perfect\" &&\n artist === \"Ed Sheeran\" &&\n sec.section === \"Chorus\" &&\n prev &&\n prev.section === \"Solo\"\n ) {\n sec.chords = [\n \"Em\", \"C\", \"G\", \"D\",\n \"Em\", \"C\", \"G\", \"D\",\n \"Em\", \"C\", \"G\", \"D\",\n \"C\", \"D\", \"G\", \"D/f#\", \"Em\", \"D\", \"C\", \"D\", \"G\"\n ];\n }\n}\n\nfor (const sec of sections) {\n if (\n title === \"Yellow\" &&\n artist === \"Coldplay\" &&\n sec.section === \"Verse I\" &&\n sec.chords[0] !== \"A\"\n ) {\n sec.chords.unshift(\"A\");\n }\n}\n\n// Blank Space (Taylor's Version): fix kendte parser-fejl fra indlejrede lyrics-bogstaver\nif (\n title === \"Blank Space (Taylor's Version)\" &&\n artist === \"Taylor Swift\"\n) {\n for (const sec of sections) {\n if (sec.section === \"Verse I\") {\n sec.chords = [\"D\", \"Bm\", \"G\", \"A\", \"D\", \"Bm\", \"G\", \"A\"];\n }\n\n if (sec.section === \"Verse II\") {\n sec.chords = [\"D\", \"Bm\", \"G\", \"A\", \"D\", \"Bm\", \"G\", \"A\"];\n }\n\n if (sec.section === \"Chorus\") {\n sec.chords = [\"D\", \"Bm\", \"Em\", \"G\", \"D\", \"Bm\", \"Em\", \"G\"];\n }\n\n if (sec.section === \"Bridge\") {\n sec.chords = [\"D\", \"Bm\", \"G\", \"A\"];\n }\n }\n}\n\nif (title === \"Wonderwall\" && artist === \"Oasis\") {\n // 1) Genskab A7sus4 i intro/vers\n for (const sec of sections) {\n if ([\"Intro\", \"Verse I\", \"Verse II\", \"Verse III\"].includes(sec.section)) {\n const repaired = [];\n for (let i = 0; i < sec.chords.length; i += 3) {\n const a = sec.chords[i];\n const b = sec.chords[i + 1];\n const c = sec.chords[i + 2];\n\n if (a === \"Em7\" && b === \"G\" && c === \"Dsus4\") {\n repaired.push(\"Em7\", \"G\", \"Dsus4\", \"A7sus4\");\n } else {\n if (a) repaired.push(a);\n if (b) repaired.push(b);\n if (c) repaired.push(c);\n }\n }\n sec.chords = repaired;\n }\n }\n\n // 2) Ret f\u00f8rste relevante chorus til Pre-Chorus og giv b\u00e5de den og Bridge de sidste 2 akkorder\n let firstPreChorusFixed = false;\n const baseWonderwallRun =\n \"Cadd9 | Dsus4 | Em7 | Cadd9 | Dsus4 | Em7 | Cadd9 | Dsus4 | G | D/f# | Em7\";\n\n for (const sec of sections) {\n const joined = sec.chords.join(\" | \");\n\n if (\n sec.section === \"Chorus\" &&\n !firstPreChorusFixed &&\n joined === baseWonderwallRun\n ) {\n sec.section = \"Pre-Chorus\";\n sec.chords = [\n \"Cadd9\", \"Dsus4\", \"Em7\",\n \"Cadd9\", \"Dsus4\", \"Em7\",\n \"Cadd9\", \"Dsus4\", \"G\", \"D/f#\", \"Em7\",\n \"G\", \"A7sus4\"\n ];\n firstPreChorusFixed = true;\n continue;\n }\n\n if (\n sec.section === \"Bridge\" &&\n joined === baseWonderwallRun\n ) {\n sec.chords = [\n \"Cadd9\", \"Dsus4\", \"Em7\",\n \"Cadd9\", \"Dsus4\", \"Em7\",\n \"Cadd9\", \"Dsus4\", \"G\", \"D/f#\", \"Em7\",\n \"G\", \"A7sus4\"\n ];\n }\n }\n}\n\n// ===== 10) filtr\u00e9r tomme sections =====\nconst cleanedSections = sections.filter(sec => sec.section && sec.chords.length > 0);\n\n// ===== 11) byg output =====\nconst out = [];\nout.push(`\ud83c\udfb5 ${title}`);\nout.push(`\ud83d\udc64 ${artist}`);\nif (capo) out.push(`\ud83c\udfb8 Capo: ${capo}`);\nout.push(\"\");\n\nfor (const sec of cleanedSections) {\n out.push(`## ${sec.section}`);\n out.push(`| ${sec.chords.join(\" | \")} |`);\n out.push(\"\");\n}\n\nreturn [\n {\n json: {\n source,\n canonical,\n found: cleanedSections.length > 0,\n title,\n artist,\n capo,\n sections: cleanedSections,\n context: out.join(\"\\n\").trim()\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
416,
176
],
"id": "82315ef7-f9d1-4d83-b64b-174fc97f2511",
"name": "Parse CW Song Payload"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nif (!item.url) {\n return [];\n}\n\nreturn [\n {\n json: item\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1760,
-16
],
"id": "84e94141-a8a6-4dd2-8b41-17a696edf770",
"name": "Save Guard"
},
{
"parameters": {
"url": "https://duckduckgo.com/html/",
"sendQuery": true,
"specifyQuery": "json",
"jsonQuery": "={\n \"q\": \"site:chordsworld.com {{$json.query || (($json.title || '') + ' ' + ($json.artist || ''))}}\"\n}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1472,
-16
],
"id": "b3e6dbe9-2d6b-4dd5-90dc-b491bb8bad93",
"name": "CW Search Request"
},
{
"parameters": {
"jsCode": "const html =\n $input.first().json.data ||\n $input.first().json.body ||\n $input.first().json.response ||\n \"\";\n\nconst queryRaw =\n $('Search Fallback Guard').first().json.query ||\n $('Set/Code Question').first().json.query ||\n \"\";\n\nconst query = (queryRaw || \"\").toLowerCase().trim();\n\nfunction safeDecode(url) {\n if (!url) return \"\";\n try {\n return decodeURIComponent(url);\n } catch {\n return url;\n }\n}\n\nfunction normalizeText(str) {\n return (str || \"\")\n .toLowerCase()\n .normalize(\"NFKD\")\n .replace(/[^\\w\\s/-]/g, \" \")\n .replace(/[-_]+/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction slugify(str) {\n return normalizeText(str).replace(/\\s+/g, \"-\");\n}\n\nfunction buildDirectCandidates(query) {\n const candidates = [];\n const q = normalizeText(query);\n\n const byMatch = q.match(/^(.*?)\\s+by\\s+(.*?)$/i);\n if (byMatch) {\n const title = byMatch[1].trim();\n const artist = byMatch[2].trim();\n\n if (title && artist) {\n const base = `${slugify(artist)}-${slugify(title)}`;\n candidates.push(`https://chordsworld.com/${base}-chord/`);\n candidates.push(`https://chordsworld.com/${base}-chords/`);\n }\n }\n\n return [...new Set(candidates)];\n}\n\nfunction slugPartsFromUrl(url) {\n const match = url.match(/chordsworld\\.com\\/([^/?#]+?)-(?:chord|chords)\\/?$/i);\n if (!match) return [];\n\n return match[1]\n .toLowerCase()\n .split(\"-\")\n .map(s => s.trim())\n .filter(Boolean);\n}\n\nfunction buildQueryParts(query) {\n const noise = new Set([\"chords\", \"chord\", \"by\", \"lyrics\", \"tabs\", \"tab\"]);\n return normalizeText(query)\n .split(\" \")\n .map(s => s.trim())\n .filter(Boolean)\n .filter(word => !noise.has(word));\n}\n\nfunction scoreCandidate(url, query) {\n const normalizedUrl = normalizeText(url);\n const queryParts = buildQueryParts(query);\n const slugParts = slugPartsFromUrl(url);\n\n let score = 0;\n\n if (/^https?:\\/\\/(?:www\\.)?chordsworld\\.com\\/.+-(?:chord|chords)(?:\\/)?$/i.test(url)) {\n score += 50;\n }\n\n const allPartsMatchSlug =\n queryParts.length > 0 &&\n queryParts.every(part => slugParts.includes(part));\n\n if (allPartsMatchSlug) {\n score += 300;\n }\n\n const allPartsMatchUrl =\n queryParts.length > 0 &&\n queryParts.every(part => normalizedUrl.includes(part));\n\n if (allPartsMatchUrl) {\n score += 150;\n }\n\n for (const part of queryParts) {\n if (slugParts.includes(part)) {\n score += 30;\n } else if (normalizedUrl.includes(part)) {\n score += 10;\n }\n }\n\n return score;\n}\n\nconst candidates = [];\n\n// 0) direct deterministic candidates first\nfor (const candidate of buildDirectCandidates(query)) {\n candidates.push(candidate);\n}\n\n// 1) uddg links from DDG\nconst uddgMatches = [...html.matchAll(/uddg=([^\"&]+)/gi)];\nfor (const match of uddgMatches) {\n const candidate = safeDecode(match[1] || \"\")\n .replace(/%2D/gi, \"-\")\n .replace(/&/g, \"&\")\n .trim();\n\n if (/^https?:\\/\\/(?:www\\.)?chordsworld\\.com\\/.+-(?:chord|chords)(?:\\/)?$/i.test(candidate)) {\n candidates.push(candidate);\n }\n}\n\n// 2) direct links fallback from HTML\nconst directMatches = [\n ...html.matchAll(/https?:\\/\\/(?:www\\.)?chordsworld\\.com\\/[^\"' <>\\s]+-(?:chord|chords)(?:\\/)?/gi)\n];\nfor (const match of directMatches) {\n candidates.push(match[0]);\n}\n\nconst uniqueCandidates = [...new Set(candidates)];\n\nlet url = \"\";\nif (uniqueCandidates.length > 0) {\n uniqueCandidates.sort((a, b) => scoreCandidate(b, query) - scoreCandidate(a, query));\n url = uniqueCandidates[0];\n}\n\nreturn [\n {\n json: {\n query,\n candidates: uniqueCandidates,\n url\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1616,
-16
],
"id": "25fa1ab4-9c07-469e-b9de-c3b0fade5702",
"name": "Finding Chords"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\n// Hvis vi allerede fandt sangen i Neo4j \u2192 RETURN\u00c9R den (ikke stop flow!)\nif (item.found === true) {\n return [\n {\n json: item\n }\n ];\n}\n\n// Ellers send query videre til ChordsWorld-s\u00f8gning\nreturn [\n {\n json: {\n query:\n item.query ||\n $('Set/Code Question').first().json.query ||\n \"\"\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1296,
-16
],
"id": "5ddfab25-8c7c-48a5-936e-191ddbc0b94e",
"name": "Search Fallback Guard"
},
{
"parameters": {
"httpMethod": "POST",
"path": "cc549916-c92c-4fc1-be35-bceb9f72c14e",
"responseMode": "lastNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
64,
-16
],
"id": "2bf0442b-be16-4132-98a7-44c9976b6ebb",
"name": "Webhook"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "7b6e4987-8193-4f88-9c90-f91e2e510b29",
"leftValue": "={{$json.found}}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1408,
-224
],
"id": "e9f00c37-b226-40c1-a630-33f12f52ced3",
"name": "If"
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\nreturn [\n {\n json: item\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1616,
-240
],
"id": "80d86e5f-de2b-4c8e-8867-5367e8a1c482",
"name": "Post Chords from Neo4j"
}
],
"connections": {
"Query Neo4j": {
"main": [
[
{
"node": "Build Context",
"type": "main",
"index": 0
}
]
]
},
"Set/Code Question": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Build Context": {
"main": [
[
{
"node": "Build Recent Result Record",
"type": "main",
"index": 0
}
]
]
},
"Format Tool Response": {
"main": [
[
{
"node": "Search Fallback Guard",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "Query Neo4j",
"type": "main",
"index": 0
}
]
]
},
"Build Recent Result Record": {
"main": [
[
{
"node": "Format Tool Response",
"type": "main",
"index": 0
}
]
]
},
"Prepare Song Payload": {
"main": [
[
{
"node": "Build Neo4j Request Body",
"type": "main",
"index": 0
}
]
]
},
"Insert Song In Neo4j": {
"main": [
[
{
"node": "Format Result",
"type": "main",
"index": 0
}
]
]
},
"Build Neo4j Request Body": {
"main": [
[
{
"node": "Insert Song In Neo4j",
"type": "main",
"index": 0
}
]
]
},
"CW HTTP Request": {
"main": [
[
{
"node": "Extract CW Raw Data",
"type": "main",
"index": 0
}
]
]
},
"Extract CW Raw Data": {
"main": [
[
{
"node": "Parse CW Song Payload",
"type": "main",
"index": 0
}
]
]
},
"Parse CW Song Payload": {
"main": [
[
{
"node": "Prepare Song Payload",
"type": "main",
"index": 0
}
]
]
},
"Save Guard": {
"main": [
[
{
"node": "CW HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"CW Search Request": {
"main": [
[
{
"node": "Finding Chords",
"type": "main",
"index": 0
}
]
]
},
"Finding Chords": {
"main": [
[
{
"node": "Save Guard",
"type": "main",
"index": 0
}
]
]
},
"Search Fallback Guard": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Set/Code Question",
"type": "main",
"index": 0
}
]
]
},
"If": {
"main": [
[
{
"node": "Post Chords from Neo4j",
"type": "main",
"index": 0
}
],
[
{
"node": "CW Search Request",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"versionId": "64df79bb-7c01-47ad-bce9-e90e6e753159",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "hBqLLLrgcrwtVcMM",
"tags": []
}
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.
httpBasicAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
search_cw_webhook. Uses httpRequest. Webhook trigger; 20 nodes.
Source: https://github.com/AndLOLGG/ai-chord-neo4j-agent/blob/92988c1277a189f2339216ab9b4eb9fc9d90ad74/n8n/search_cw_webhook.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.
Jigsaw API key for image processing, I use this as a gatekeeper/second pair of eyes. LINK to their website https://jigsawstack.com/ SECOND A postgress DATABASE (I use Supabase) LlamaCloud for the pars
W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 48 nodes.
Whatsapp Multi Agent System optimized copy 2.0. Uses airtable, httpRequest, errorTrigger. Webhook trigger; 44 nodes.
Invoice Agent. Uses httpRequest, emailSend. Webhook trigger; 29 nodes.
Reputation Engine — SEO QA Agent. Uses httpRequest. Webhook trigger; 28 nodes.