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": "Tagging_sub",
"nodes": [
{
"parameters": {
"model": "google/gemini-2.5-pro",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"typeVersion": 1,
"position": [
368,
304
],
"id": "d74fa437-2971-4e5d-b735-3bbb11ff748e",
"name": "OpenRouter Chat Model",
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"promptType": "define",
"text": "=Du bist der REFEREE. Deine Aufgabe ist es, f\u00fcr eine Liste von Schlagworten (Batch) die jeweils korrekten GND-IDs zu finden.\n\nDEINE STRATEGIE\nDu erh\u00e4ltst einen Batch von bis zu 10 Schlagworten.\n\nAnalysiere jedes Schlagwort einzeln im Kontext des Objekts (Caption & Metadaten) UND nutze die mitgelieferten Felder rationale und judge_context zwingend zur Disambiguierung, falls ein Begriff mehrere unterschiedliche Bedeutungen hat.\n\nNutze das Tool search_gnd f\u00fcr jedes Keyword, um in der lokalen Datenbank nach Normdaten zu suchen. Du kannst und sollst mehrere Tool-Calls parallel oder hintereinander absetzen.\n\nWICHTIG: Das Tool durchsucht vorrangig Sachbegriffe (Materialien, Objekte, historische Konzepte, abstrakte Dinge).\n\n\u00dcbergib dem Tool den Suchbegriff. Wenn der Begriff mehrdeutig ist, kannst du auch Synonyme testen.\n\nValidiere die Tool-Ergebnisse gegen den visuellen Befund und die Metadaten. Achte im Tool-Ergebnis besonders auf das Feld \"definition\", um abzugleichen, ob es sich um das richtige Konzept handelt.\n\nFAST-FAIL REGELN (Zeit-Optimierung)\nBevor du das Tool nutzt oder tief nachdenkst, wende diese \"Short-Circuit\"-Logik an:\n\nZUSTANDS- UND QUALIT\u00c4TSBEGRIFFE:\nBegriffe wie \"besch\u00e4digt\", \"abgenutzt\", \"fleckig\", \"Kratzer\", \"unscharf\" oder \"r\u00f6tlich\" sind rein deskriptiv. Die GND f\u00fchrt hierf\u00fcr meist keine Sachschlagworte f\u00fcr die Bilderschlie\u00dfung.\n-> AKTION: Setze sofort \"gnd_id\": null und \"gnd_confidence\": \"no_match\". KEIN Tool-Call n\u00f6tig!\n\n1-TOOL-LIMIT F\u00dcR SACHBEGRIFFE:\nWenn eine Suche in search_gnd keine Treffer liefert, die EXAKT oder SEHR NAHE am Keyword liegen:\n-> AKTION: Suche NICHT endlos weiter. Setze \"gnd_id\": null.\n\nCLUSTER-FILTER:\nKeywords aus dem Cluster \"Visuelle_Merkmale\" (au\u00dfer konkrete Techniken wie \"Sepia\") sind fast immer \"no_match\". Pr\u00fcfe sie extrem streng und brich bei Zweifeln sofort ab.\n\nOUTPUT FORMAT (STRENGSTENS EINZUHALTEN)\nAntworte AUSSCHLIESSLICH mit einem validen JSON-Array. Jedes Element im Array entspricht einem Keyword aus dem Eingabe-Batch.\nDu musst die Ursprungsdaten (Schlagwort, Begr\u00fcndungen von LLM2 und LLM3) 1:1 aus dem Input-Batch \u00fcbernehmen und um deine eigenen GND-Ergebnisse erg\u00e4nzen.\n\nNutze exakt diese Struktur f\u00fcr jedes Item im Array:\n\n[\n{\n\"source_cluster\": \"Der Name des Clusters aus dem Input\",\n\"keyword_original\": \"Das 'keyword' aus dem Input-Batch\",\n\"llm2_rationale\": \"Das Feld 'rationale' aus dem Input-Batch (1:1 kopieren)\",\n\"llm3_judge_comment\": \"Das Feld 'judge_context' aus dem Input-Batch (1:1 kopieren)\",\n\"gnd_id\": \"4130439-1\",\n\"gnd_preferred_name\": \"Name laut GND (oder null)\",\n\"gnd_confidence\": \"high\" | \"medium\" | \"low\" | \"no_match\",\n\"llm4_reasoning\": \"Deine eigene Begr\u00fcndung, warum diese GND-ID passt oder warum keine gefunden wurde.\",\n\"additional_data\": {\n\"alternate_names\": \"String (kommaseparierte Synonyme laut GND oder null)\",\n\"definition\": \"String (die Definition laut GND oder null)\"\n}\n}\n]\n\nWICHTIG:\n\n\u00dcbernehme \"source_cluster\", \"keyword_original\", \"llm2_rationale\" und \"llm3_judge_comment\" EXAKT und ohne inhaltliche \u00c4nderungen aus deinem Input.\n\nDie Felder in \"additional_data\" nimmst du aus dem Output des Tools \"search_gnd\".\n\nKEIN Markdown (wie json ... ), nur das rohe Array, beginnend mit [ und endend mit ].",
"hasOutputParser": true,
"options": {
"systemMessage": "===================================\n--- DER BATCH (10 SCHLAGWORTE) ---\n==================================\n\nHier sind die zu pr\u00fcfenden Keywords und ihre Cluster:\n{{ JSON.stringify($json.keyword_batch) }}\n\n==================================\n--- KONTEXT (ZUR ENTSCHEIDUNGSHILFE) ---\n==================================\n\nVISUELLER BEFUND (Caption):\n\"{{ $json.caption }}\"\n\nMETADATEN (Hintergrund):\n\"{{ $json.metadata }}\"\n\n==================================\n--- DEINE AUFGABE ---\n==================================\n\nBearbeite jetzt alle Schlagworte im Batch. F\u00fchre f\u00fcr jedes eine GND-Suche durch und gib das validierte JSON-Array zur\u00fcck.",
"returnIntermediateSteps": true,
"passthroughBinaryImages": true
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 2.2,
"position": [
608,
64
],
"id": "c9bb91c9-10b2-42a1-a7e9-cd87bae52167",
"name": "AI Agent",
"alwaysOutputData": true,
"retryOnFail": true,
"waitBetweenTries": 5000,
"onError": "continueRegularOutput"
},
{
"parameters": {
"amount": 3
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
1008,
288
],
"id": "72c3535b-b810-46d4-a378-34b74bf288b5",
"name": "Wait"
},
{
"parameters": {
"inputSource": "passthrough"
},
"id": "eb6fb377-b283-4deb-8032-d8f3352f3aa2",
"typeVersion": 1.1,
"name": "Start",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
-240,
0
]
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
368,
0
],
"id": "afe98793-98f7-48a2-a173-19322766fad3",
"name": "Loop Over Items"
},
{
"parameters": {
"jsCode": "/*\n=========================================================\nAGENT BATCH PARSER (v7 - Lineage & Multi-Batch Support)\n- Iteriert \u00fcber ALLE Batches (nicht nur den ersten)\n- Extrahiert JSON-Array aus Agenten-String\n- Handhabt Markdown-Backticks & Zeilenumbr\u00fcche\n- Extrahiert die Data Lineage (LLM2, 3 und 4)\n=========================================================\n*/\n\nconst items = $input.all();\nconst allParsedResults = [];\n\nfor (const itemWrapper of items) {\n const item = itemWrapper.json;\n const outputText = item.output || item.text || \"\"; \n\n try {\n // 1. Suche den Start und das Ende des Arrays im String\n const arrayStart = outputText.indexOf('[');\n const arrayEnd = outputText.lastIndexOf(']');\n \n if (arrayStart === -1 || arrayEnd === -1) {\n allParsedResults.push({\n json: { error: \"Kein JSON-Array im Output\", raw_output: outputText }\n });\n continue; // Mache mit dem n\u00e4chsten Batch weiter\n }\n\n const cleanJsonString = outputText.substring(arrayStart, arrayEnd + 1);\n const agentResults = JSON.parse(cleanJsonString);\n\n // 2. Jedes Ergebnis im Array als n8n-Item vereinzeln\n for (const res of agentResults) {\n allParsedResults.push({\n json: {\n // IDs aus dem Flow-Kontext\n parent_id: item.parent_id || item.ID || \"N/A\", \n \n // Identifikation\n keyword_original: res.keyword_original || res.keyword || \"N/A\",\n source_cluster: res.source_cluster || \"Unsorted\",\n \n // GND Daten (mit Fallbacks auf alte Nomenklatur)\n gnd_id: res.gnd_id || null,\n preferred_name: res.gnd_preferred_name || res.preferred_name || null,\n confidence: res.gnd_confidence || res.confidence || \"no_match\",\n \n // DATA LINEAGE (Die Begr\u00fcndungen der LLMs)\n llm2_rationale: res.llm2_rationale || null,\n llm3_judge_comment: res.llm3_judge_comment || null,\n llm4_reasoning: res.llm4_reasoning || res.reasoning || null,\n \n // Metadaten flachklopfen\n meta_start_date: res.additional_data?.start_date || null,\n meta_end_date: res.additional_data?.end_date || null,\n meta_role: res.additional_data?.role_or_profession || null,\n meta_location: res.additional_data?.location_primary || null\n }\n });\n }\n\n } catch (e) {\n // Fehlerfall f\u00fcr diesen spezifischen Batch\n allParsedResults.push({\n json: {\n error: \"Parsing Error\",\n message: e.message,\n raw_output: outputText\n }\n });\n }\n}\n\nreturn allParsedResults;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
992,
64
],
"id": "67e53c20-6b06-409b-857e-7104885bf84f",
"name": "Code in JavaScript1"
},
{
"parameters": {
"jsCode": "/*\n=========================================================\nFINAL AGGREGATOR (v19 - Post-Parser)\n- Aggregiert die vom Parser vereinzelten Items\n- Gruppiert nach 'source_cluster'\n=========================================================\n*/\n\nconst items = $input.all();\nconst enrichedResult = {};\n\nfor (const itemWrapper of items) {\n const data = itemWrapper.json;\n \n // Fehlerhafte Items \u00fcberspringen\n if (data.error) continue;\n \n const cluster = data.source_cluster || \"Unsorted\";\n if (!enrichedResult[cluster]) {\n enrichedResult[cluster] = [];\n }\n\n // Eintrag zusammenbauen\n const entry = {\n term: data.keyword_original,\n gnd_name: data.preferred_name,\n gnd_id: data.gnd_id,\n confidence: data.confidence,\n \n // Data Lineage\n llm2_rationale: data.llm2_rationale,\n llm3_judge_comment: data.llm3_judge_comment,\n llm4_reasoning: data.llm4_reasoning,\n \n // Metadaten (nur falls vorhanden)\n ...(data.meta_start_date && { born_founded: data.meta_start_date }),\n ...(data.meta_end_date && { died_dissolved: data.meta_end_date }),\n ...(data.meta_role && { role_type: data.meta_role }),\n ...(data.meta_location && { location: data.meta_location })\n };\n\n enrichedResult[cluster].push(entry);\n}\n\nreturn {\n json: {\n gnd_result_string: JSON.stringify(enrichedResult, null, 2)\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
608,
-96
],
"id": "f3fe7d54-0ece-4c00-a58d-aaa050713337",
"name": "Code in JavaScript2"
},
{
"parameters": {
"jsCode": "/*\n=========================================================\nBATCH SPLITTER (FINAL VERSION)\n- Gruppiert Keywords in 10er Batches\n- Extrahiert 'term', 'why' (rationale) & 'judge_comment' (judge_context)\n- Filtert technische Systemfelder aus den Metadaten\n- Sichere Fallbacks f\u00fcr Caption & Metadata\n=========================================================\n*/\n\nconst items = $input.all();\nconst outputBatches = [];\nconst BATCH_SIZE = 10;\n\n// Felder, die aus den Metadaten gel\u00f6scht werden sollen\nconst excludeKeys = [\n \"tags_gemini_2.5_pro\",\n \"_archived\",\n \"_creator\",\n \"_ctime\",\n \"_last_modifier\",\n \"_mtime\"\n];\n\n// Hilfsfunktion zur Reinigung der Metadaten-Strings\nfunction cleanMetadata(rawString) {\n if (!rawString) return \"\";\n return rawString\n .split('\\n')\n .filter(line => !excludeKeys.some(key => line.trim().startsWith(key + \":\")))\n .join('\\n')\n .trim();\n}\n\nconst allowedClusters = [\n \"Objekttyp\", \"Thema_Ph\u00e4nomen\", \"Inhalt_Motiv\", \"Funktion_Zweck\",\n \"Visuelle_Merkmale\", \"Form_Gestalt\", \"Bestandteile\", \"Gebrauchskontext\",\n \"Kultureller_Kontext\", \"Emotion_Atmosph\u00e4re\", \"Farbe_Nuancen\"\n];\n\nfor (const item of items) {\n const json = item.json;\n \n // Sichere Fallbacks: Greift alle m\u00f6glichen Schreibweisen ab\n const rawCaption = json.Caption || json.caption || \"\";\n const rawMeta = json.Metadata || json.metadata || json.context_data_string || \"\";\n \n // Metadaten bereinigen\n const cleanedMeta = cleanMetadata(rawMeta);\n\n // Keywords und Begr\u00fcndungen sammeln\n const allKeywords = [];\n for (const cluster of allowedClusters) {\n let keywordArray = json[cluster]; \n \n if (!keywordArray) continue;\n\n if (typeof keywordArray === 'string') {\n try { keywordArray = JSON.parse(keywordArray); } catch (e) { keywordArray = []; }\n }\n\n if (Array.isArray(keywordArray)) {\n keywordArray.forEach(kw => {\n if (kw && kw.term && typeof kw.term === 'string' && kw.term.trim() !== \"\") {\n allKeywords.push({\n keyword: kw.term.trim(),\n source_cluster: cluster,\n rationale: kw.why || \"\", // Neu: LLM2 Begr\u00fcndung\n judge_context: kw.judge_comment || \"\" // Neu: LLM3 Urteil\n });\n }\n });\n }\n }\n\n // In 10er Batches aufteilen und Output generieren\n for (let i = 0; i < allKeywords.length; i += BATCH_SIZE) {\n const batchChunk = allKeywords.slice(i, i + BATCH_SIZE);\n \n outputBatches.push({\n json: {\n keyword_batch: batchChunk, \n caption: rawCaption, \n metadata: cleanedMeta, \n batch_info: {\n current: Math.floor(i / BATCH_SIZE) + 1,\n total: Math.ceil(allKeywords.length / BATCH_SIZE)\n }\n }\n });\n }\n}\n\nreturn outputBatches;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
160,
0
],
"id": "813a8968-f61e-411d-892a-57c47d108ce5",
"name": "Code in JavaScript3"
},
{
"parameters": {
"toolDescription": "Der exakte Suchbegriff f\u00fcr die GND. Bitte nur das nackte Wort \u00fcbergeben (z.B. Preu\u00dfen oder Handschrift). Keine Sonderzeichen, keine Lucene-Syntax und keine Formatierungen!",
"method": "POST",
"url": "http://gnd_elasticsearch:9200/gnd_index/_search",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"size\": 5,\n \"query\": {\n \"multi_match\": {\n \"query\": \"{{ $fromAI('suchbegriff', 'Der exakte Suchbegriff f\u00fcr die GND. Bitte nur das nackte Wort \u00fcbergeben (z.B. Preu\u00dfen oder Handschrift).') }}\",\n \"fields\": [\n \"preferred_name^4\",\n \"alternate_names^3\",\n \"definition^1\"\n ],\n \"fuzziness\": \"AUTO\"\n }\n }\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequestTool",
"typeVersion": 4.4,
"position": [
704,
304
],
"id": "50ca3d34-b2bb-4592-9656-9297b2318b3b",
"name": "search_gnd"
}
],
"connections": {
"OpenRouter Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Code in JavaScript1",
"type": "main",
"index": 0
}
]
]
},
"Start": {
"main": [
[
{
"node": "Code in JavaScript3",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "Code in JavaScript2",
"type": "main",
"index": 0
}
],
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript1": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript3": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"search_gnd": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"versionId": "f7fbcfc3-03f3-465f-a204-892cd027b036",
"id": "132peiuMeOoe9GA2Pcg91",
"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.
openRouterApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Tagging_sub. Uses lmChatOpenRouter, agent, executeWorkflowTrigger, httpRequestTool. Event-driven trigger; 9 nodes.
Source: https://github.com/sebastianruffberlin/AI-Museum-Tagging/blob/8338053d8ebe7c5b335249bd73cb4cf8f05881c4/workflows/Tagging_sub.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.
The AI-Powered Shopify SEO Content Automation is an enterprise-grade workflow that transforms product content creation for e-commerce stores. This sophisticated multi-agent system integrates GPT-4o, C
Who is this for? Agencies, consultants, and service providers who conduct discovery calls and need to quickly turn conversations into professional proposals.
Ultimate Browser Agent. Uses airtopTool, slack, airtop, executeWorkflowTrigger. Event-driven trigger; 24 nodes.
How it works Listens to Telegram messages to detect stock-related queries. Extracts company name and identifies its exact stock ticker symbol. Searches Yahoo Finance for stock info using the ticker. F
Deep Research new (fr). Uses outputParserStructured, formTrigger, chainLlm, form. Event-driven trigger; 82 nodes.