This workflow follows the Execute Workflow Trigger → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "02_LLM_Pipeline v1.0",
"nodes": [
{
"parameters": {
"workflowInputs": {
"values": [
{
"name": "row_json",
"type": "any"
},
{
"name": "row_scan_context",
"type": "any"
},
{
"name": "page_scan",
"type": "any"
},
{
"name": "page_json",
"type": "any"
},
{
"name": "year_span",
"type": "any"
},
{
"name": "_id",
"type": "any"
},
{
"name": "previous_row_corrected",
"type": "any"
},
{
"name": "previous_rows_context_string",
"type": "any"
},
{
"name": "config"
},
{
"name": "validierung_buchstaben"
},
{
"name": "row_scan_magenta"
},
{
"name": "schreiber_beschreibungen",
"type": "any"
}
]
}
},
"id": "c9c7492d-8ee1-47a2-8355-ea58df70a0c2",
"typeVersion": 1.1,
"name": "TRG_Start",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
2736,
256
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "3c080dfa-535e-4bee-b8f6-127b912e6172",
"name": "rawSystempromptText",
"value": "==Du bist ein Provenienz-Ermittler f\u00fcr historische Museumsinventare. Deine Aufgabe ist die Rekonstruktion des Erwerbsereignisses aus einem handschriftlichen Inventarbucheintrag. Die Buch-Identifikation und den Sammlungskontext findest du in der Konfiguration im User-Prompt.\n\nDu arbeitest mit bereits extrahierten und GND-validierten Daten. Dein Output sind ausschlie\u00dflich die flachen Provenienz- und Altbestand-Felder im definierten JSON-Schema.\n\nSAMMLUNGSKONTEXT: siehe buch_identifikation und kontext_regeln in der Konfiguration im User-Prompt (geografischer Schwerpunkt, Datumsfenster, Sammlungsschwerpunkt).\n\nGOLD-STANDARD (explizit): Ein Trigger-Wort ist vorhanden (gekauft, geschenkt, remis., \u00fcbernommen, Ankauf, Schenkung, Auktion...) \u2192 vollst\u00e4ndiges Provenienz-Ereignis extrahieren.\n\nSILBER-STANDARD (implizit): Kein Trigger-Wort, aber Indizien vorhanden\n(Name, Datum, Preis in Provenienz-Spalte) \u2192 Indizien extrahieren,\nprovenienz_ereignis_typ MUSS \"<unbekannt>\" bleiben \u2014 auch wenn Preis\nund Name gleichzeitig vorhanden sind. Ohne Trigger-Wort ist kein\nEreignistyp ableitbar.\n\nHARTE VERBOTE:\n- Das Inventarisierungsdatum (eintragsdatum_iso) darf NIEMALS in provenienz_datum_iso_von\n- Objektdatierungen (\"um 1900\", \"18. Jh.\") sind KEINE Provenienz-Daten\n- Bei Unsicherheit: leer lassen statt raten\n- Wenn der Schrift-Validator-Befund (im User-Prompt) einen ZWEIFEL\n f\u00fcr die Provenienz- oder Preis-Spalte markiert: Wert nur \u00fcbernehmen\n wenn er durch GND-Entit\u00e4ten oder visuelle Pr\u00fcfung best\u00e4tigt ist.\n- Die Spalte mit role_kuenstler_und_adresse enth\u00e4lt den K\u00fcnstler/Einlieferer \u2014\n NIEMALS automatisch als Verk\u00e4ufer \u00fcbernehmen ohne explizites Triggerwort.\n Der K\u00fcnstler ist nicht zwingend der Verk\u00e4ufer.\n\nALTBESTAND-KLASSIFIKATION:\nDie buchspezifischen Altbestand-Hinweise findest du im User-Prompt\nunter kontext_regeln.provenienz_regeln.altbestand_hinweis.\n- Wenn LLM6 audit_funde (im User-Prompt unter SEKUND\u00c4RE INVENTARNUMMERN)\n Eintr\u00e4ge enth\u00e4lt (audit_funde.length > 0) UND die gefundenen Systeme\n dem altbestand_hinweis entsprechen\n \u2192 altbestand_ist: true, kategorie: \"reinventarisierung\"\n- Wenn LLM3 weitere_inventarnummern Eintr\u00e4ge enth\u00e4lt (im User-Prompt\n unter INVENTARNUMMER) UND die Systeme dem altbestand_hinweis entsprechen\n \u2192 altbestand_ist: true, kategorie: \"reinventarisierung\"\n- Wenn expliziter Vermerk \"Altbestand\" / \"Alter Besitz\"\n \u2192 altbestand_ist: true, kategorie: \"explizit\"\n- Sonst \u2192 altbestand_ist: false, kategorie: \"keiner\"\n\nPROVENIENZ-THESAURUS (normierte Hauptbegriffe, Gruppe 1-3):\nAnkauf | Auktion | Schenkung | Schenkung unter Auflage | Verm\u00e4chtnis | Erbe | Tausch | \u00dcbereignung | \u00dcbernahme | Leihgabe | Dauerleihgabe | Verwahrung | Beschlagnahmung_NS_Kontext | Zwangsverkauf_NS_Kontext | Arisierung | Beutekunst | Restitution | \u00dcbernahme_Volkseigentum | Bodenreform | Enteignung_Republikflucht | Sequestration_DDR | Kolonialerwerbung\n\nVARIANTEN \u2192 NORMIERUNG (Beispiele):\n\"angekauft\" \u2192 Ankauf | \"geschenkt\" \u2192 Schenkung | \"remis.\" / \"remittiert\" \u2192 Ankauf | \"\u00fcbernommen\" \u2192 \u00dcbernahme | \"gestiftet\" \u2192 Schenkung | \"testamentarisch\" \u2192 Verm\u00e4chtnis | \"Nachlass\" \u2192 Erbe | \"versteigert\" \u2192 Auktion | \"getauscht\" \u2192 Tausch\n\nW\u00c4HRUNGEN: Die erwarteten W\u00e4hrungen findest du im User-Prompt\nunter kontext_regeln.provenienz_regeln.erwartete_waehrung und waehrung_alternativ.\n\nVISUELLE ANALYSE:\nDu erh\u00e4ltst den Scan der Zeile. Analysiere gezielt:\n- Die Spalte mit der Provenienz-Rolle (gem\u00e4\u00df spalten_zuweisung im User-Prompt)\n- Die Spalte mit der Preis-Rolle (gem\u00e4\u00df spalten_zuweisung im User-Prompt)\n- Randbereiche und Stempel auf Provenienz-Vermerke\n- Rote Schrift / Nachtr\u00e4ge die OCR nicht erfasst hat\n\nAUSGABE: Ausschlie\u00dflich valides JSON-Objekt ohne Markdown, exakt nach folgendem Schema:\n\nAUSGABE-SCHEMA (exakt diese Felder, kein Markdown, kein Freitext):\n{\n \"provenienz_ereignis_typ\": \"Ankauf | Schenkung | Auktion | ... | <unbekannt>\",\n \"provenienz_datum_iso_von\": \"YYYY-MM-DD | YYYY-MM | YYYY | <unbekannt>\",\n \"provenienz_datum_iso_bis\": null,\n \"provenienz_preis_wert\": 500.00,\n \"provenienz_preis_waehrung\": \"DM | RM | Mark | <unbekannt>\",\n \"provenienz_verkaufer_name\": \"Name | <unbekannt> | <nicht_anwendbar>\",\n \"provenienz_verkaufer_typ\": \"Person | Institution | null\",\n \"provenienz_verkaufer_gnd_id\": \"GND-ID | null\",\n \"provenienz_schenkender_name\": \"Name | <unbekannt> | <nicht_anwendbar>\",\n \"provenienz_schenkender_typ\": \"Person | Institution | null\",\n \"provenienz_schenkender_gnd_id\": \"GND-ID | null\",\n \"provenienz_transaktionsort_name\": \"Ort | <unbekannt>\",\n \"provenienz_transaktionsort_gnd_id\": \"GND-ID | null\",\n \"provenienz_weitere_akteure_namen\": \"Name1; Name2 | <nicht_anwendbar>\",\n \"provenienz_begruendung\": \"Kurze Begr\u00fcndung max 30 W\u00f6rter\",\n \"prov_rekonstruktionstext\": \"Lesbarer Satz f\u00fcr Forscher z.B. Erworben von X f\u00fcr Y DM\",\n \"prov_vollstaendige_kette\": [{\"datum\": \"...\", \"ereignis\": \"...\", \"akteur\": \"...\"}],\n \"altbestand_ist\": true,\n \"altbestand_kategorie\": \"reinventarisierung | explizit | keiner\",\n \"altbestand_indikatoren\": \"z.B. Sekund\u00e4rnummer aus hist. Nummernkreis (aus Config)\",\n \"altbestand_confidence\": 0.9\n}\n\nREGELN F\u00dcR NICHT-ANWENDBARE FELDER:\n- Ereignistyp Ankauf \u2192 provenienz_schenkender_* = \"<nicht_anwendbar>\"\n- Ereignistyp Schenkung \u2192 provenienz_verkaufer_* = \"<nicht_anwendbar>\"\n- Keine weiteren Akteure \u2192 provenienz_weitere_akteure_namen = \"<nicht_anwendbar>\"\n- Kein Preis erkennbar \u2192 provenienz_preis_wert = null, provenienz_preis_waehrung = null",
"type": "string"
},
{
"id": "6f961d5d-fc9f-4aa9-abec-f1c5863e6703",
"name": "rawPromptText",
"value": "==Analysiere diesen Inventarbucheintrag auf Provenienz-Informationen.\n\n=== BUCH-KONFIGURATION ===\n{{ $json.Input.config_sektion_zwei }}\n\nBUCHSPEZIFISCHE PROVENIENZ-PARAMETER:\n- Erwartete W\u00e4hrung: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.provenienz_regeln?.erwartete_waehrung || 'unbekannt' }}\n- Alternative W\u00e4hrungen: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.provenienz_regeln?.waehrung_alternativ || []) }}\n- Altbestand-Hinweis: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.provenienz_regeln?.altbestand_hinweis || 'kein Hinweis' }}\n- Datumsfenster: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.datumsfenster_iso || 'unbekannt' }}\n- Datum-Warnung: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.provenienz_regeln?.datum_warnung || '' }}\n\n=== SPALTEN-ZUWEISUNG (DYNAMISCH) ===\n{{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).spalten_zuweisung || {}) }}\n\n=== BEREITS EXTRAHIERTE DATEN (aus vorherigen Pipeline-Schritten) ===\n\nALLE SPALTEN (final):\n{{ JSON.stringify($json['LLM 4'].spalten_vollstaendig_final) }}\n\nINVENTARNUMMER: {{ $json['LLM 3'].inventarnummer.normalisiert }} | Eintragsdatum: {{ $json['LLM 4'].eintrag_metadata.eintrag_datum_iso }}\n\nSEKUND\u00c4RE INVENTARNUMMERN (von LLM6 visuell gefunden):\n{{ JSON.stringify($json['LLM 6'].audit_funde || []) }}\n\nGND-VALIDIERTE ENTIT\u00c4TEN (von LLM5):\n{{ JSON.stringify($json['LLM 5']) }}\n\n=== SCHRIFT-VALIDATOR BEFUND ===\n{{ JSON.stringify($json.Input.validierung) }}\n\nDer Befund markiert Stellen wo Transkribus m\u00f6glicherweise\nfalsch gelesen hat. F\u00fcr deine Provenienz-Analyse:\n- Identifiziere Provenienz- und Preis-Spalte \u00fcber spalten_zuweisung oben\n- Wenn diese Spalten einen ZWEIFEL tragen: erh\u00f6hte Vorsicht \u2014\n im Zweifel leer lassen statt falschen Wert \u00fcbernehmen\n- F\u00fcr Namen: bevorzuge den GND-validierten official_name\n aus LLM5 gegen\u00fcber dem Spaltentext\n\n=== AUFGABE ===\n\nLies die spalten_zuweisung aus der Buch-Konfiguration.\nIdentifiziere welche Spalte die Provenienz-Rolle und welche die Preis-Rolle hat.\nF\u00fchre dann die dreistufige Provenienz-Analyse durch:\n\n1. Prim\u00e4r-Analyse: Provenienz-Spalte + Preis-Spalte auf Gold/Silber-Standard pr\u00fcfen\n2. Sekund\u00e4r-Analyse: alle anderen Spalten auf zus\u00e4tzliche Indizien\n3. Synthese: zu einem koh\u00e4renten Provenienz-Ereignis zusammenf\u00fchren\n\nNutze die GND-validierten Personen/Institutionen f\u00fcr die Rollen-Zuordnung.\nBeachte das visuelle Bild \u2014 rote Nachtr\u00e4ge und Stempel k\u00f6nnen Provenienz-Vermerke enthalten die im OCR fehlen.\n\nAntworte ausschlie\u00dflich mit dem JSON-Objekt gem\u00e4\u00df Output-Schema.",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "2b98afb5-8583-48e6-a450-cbfb3ba552a7",
"name": "SET_LLM7_Prompt",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2976,
3616
]
},
{
"parameters": {
"jsCode": "// \u2550\u2550\u2550 REQ_LLM7_Provenienz | v1.0 \u2550\u2550\u2550\n// Zweck: Baut LLM7-Request (Provenienz, qwen) inkl. Bild\n\n// LLM7: Provenienz-Extraktion (Vision + Text) \u2014 qwen3.6-27b-mtp\n// Dense-Modell f\u00fcr pr\u00e4zise strukturierte Extraktion mit Vision.\n// Nutzt vorberechneten base64 aus FN_Load_Images \u2014 kein erneuter Download.\nconst item = $input.item.json;\n// FIX Bug 1: Bilder direkt aus 'Input Images1' beziehen (Set-Nodes schleifen sie nicht durch)\nconst __imgSrc = $('FN_Load_Images').item.json;\nif (!item._row_scan_magenta_data_url) item._row_scan_magenta_data_url = __imgSrc._row_scan_magenta_data_url;\nif (!item._context_scan_data_url) item._context_scan_data_url = __imgSrc._context_scan_data_url;\n\nif (!item.rawPromptText || !item.rawSystempromptText) {\n throw new Error('Input-Felder unvollst\u00e4ndig: rawPromptText oder rawSystempromptText fehlt!');\n}\n\nconst userContent = [{ type: 'text', text: item.rawPromptText }];\n\n// Vision: row_scan_magenta als prim\u00e4res Bild (Spalten-Grenzen sichtbar)\nif (item._row_scan_magenta_data_url) {\n userContent.push({ type: 'text', text: '\\n### VISUELLER SCAN (mit Spalten-Grenzen):\\nDie magentafarbenen Linien zeigen Spalten-Grenzen \u2014 KEIN inhaltlicher Bestandteil. Analysiere gezielt Spalte 6 (Provenienz) und Spalte 7 (Preis) sowie Randbereiche auf rote Nachtr\u00e4ge und Stempel:' });\n userContent.push({ type: 'image_url', image_url: { url: item._row_scan_magenta_data_url } });\n}\n\nreturn {\n json: {\n model: 'qwen3.6-27b-mtp',\n response_format: { type: 'json_object' },\n messages: [\n { role: 'system', content: item.rawSystempromptText },\n { role: 'user', content: userContent }\n ],\n temperature: 0.1,\n top_p: 0.8,\n top_k: 20,\n presence_penalty: 0.0,\n frequency_penalty: 0.0,\n max_tokens: 4096,\n reasoning_budget: 2048,\n chat_template_kwargs: { enable_thinking: false }\n }\n};"
},
"id": "8221ab71-ab51-45de-afc3-04a2c573cb7b",
"name": "REQ_LLM7_Provenienz",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3200,
3616
]
},
{
"parameters": {
"jsCode": "// \u2550\u2550\u2550 PARSE_LLM7 | v1.0 \u2550\u2550\u2550\n// Zweck: Parst LLM7-Response \u2192 'LLM 7'-Objekt\n\n// LLM7 Response \u2192 als 'LLM 7' Objekt in die Pipeline-Struktur einbauen\nconst item = $input.item.json;\n\nlet raw = item.choices?.[0]?.message?.content || '{}';\nif (typeof raw !== 'string') raw = JSON.stringify(raw);\n\nlet parsed = {};\ntry {\n const clean = raw.replace(/```json|```/g, '').trim();\n parsed = JSON.parse(clean);\n} catch(e) {\n parsed = {};\n item._error = true; item._error_source = 'Parser LLM 7'; // FIX Bug 6\n}\n\nreturn {\n json: {\n ...item,\n 'LLM 7': {\n provenienz_ereignis_typ: parsed.provenienz_ereignis_typ ?? '',\n provenienz_datum_iso_von: parsed.provenienz_datum_iso_von ?? '',\n provenienz_datum_iso_bis: parsed.provenienz_datum_iso_bis ?? null,\n provenienz_preis_wert: parsed.provenienz_preis_wert ?? null,\n provenienz_preis_waehrung: parsed.provenienz_preis_waehrung ?? '',\n provenienz_verkaufer_name: parsed.provenienz_verkaufer_name ?? '',\n provenienz_verkaufer_typ: parsed.provenienz_verkaufer_typ ?? null,\n provenienz_verkaufer_gnd_id: parsed.provenienz_verkaufer_gnd_id ?? null,\n provenienz_schenkender_name: parsed.provenienz_schenkender_name ?? '',\n provenienz_schenkender_typ: parsed.provenienz_schenkender_typ ?? null,\n provenienz_schenkender_gnd_id: parsed.provenienz_schenkender_gnd_id ?? null,\n provenienz_transaktionsort_name: parsed.provenienz_transaktionsort_name ?? '',\n provenienz_transaktionsort_gnd_id: parsed.provenienz_transaktionsort_gnd_id ?? null,\n provenienz_weitere_akteure_namen: parsed.provenienz_weitere_akteure_namen ?? '',\n provenienz_begruendung: parsed.provenienz_begruendung ?? '',\n prov_rekonstruktionstext: parsed.prov_rekonstruktionstext ?? '',\n prov_vollstaendige_kette: parsed.prov_vollstaendige_kette ?? [],\n altbestand_ist: parsed.altbestand_ist ?? false,\n altbestand_kategorie: parsed.altbestand_kategorie ?? 'keiner',\n altbestand_indikatoren: parsed.altbestand_indikatoren ?? '',\n altbestand_confidence: parsed.altbestand_confidence ?? null,\n },\n llm7_tokens: item.usage?.total_tokens ?? 0\n }\n};"
},
"id": "7d422c4e-f21f-456f-8af9-42011371ca6a",
"name": "PARSE_LLM7",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3648,
3616
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
"name": "rawPromptText",
"value": "======================================================================\nUSER-INPUT: AKTUELLE KONFIGURATION & ZEILENDATEN\n=====================================================================\n### 1. BUCH-KONFIGURATION (DEIN GESETZ)\n{{ $('SET_Input_Row').item.json.config_sektion_zwei }}\n\n### 2. ROHDATEN DER AKTUELLEN ZEILE\n- Spalte 1: {{ $('SET_Input_Row').item.json.row_spalte_1 }}\n- Spalte 2: {{ $('SET_Input_Row').item.json.row_spalte_2 }}\n- Spalte 3: {{ $('SET_Input_Row').item.json.row_spalte_3 }}\n- Spalte 4: {{ $('SET_Input_Row').item.json.row_spalte_4 }}\n- Spalte 5: {{ $('SET_Input_Row').item.json.row_spalte_5 }}\n- Spalte 6: {{ $('SET_Input_Row').item.json.row_spalte_6 }}\n- Spalte 7: {{ $('SET_Input_Row').item.json.row_spalte_7 }}\n- Spalte 8: {{ $('SET_Input_Row').item.json.row_spalte_8 }}\n- Spalte 9: {{ $('SET_Input_Row').item.json.row_spalte_9 }}\n- Spalte 10: {{ $('SET_Input_Row').item.json.row_spalte_10 }}\n\n### 3. VISUELLE REFERENZ (BILD-ANALYSE)\nDie zugeh\u00f6rigen Scans (Detail-Scan der Zeile und der erweiterte Kontext-Scan) sind dieser Nachricht als direkte Bildanh\u00e4nge unten beigef\u00fcgt. Gleiche das Bild zwingend mit den Rohdaten aus Sektion 2 ab (Achte besonders auf DITO-Zeichen, Rote Tinte und Streichungen).\n\n### 4. DEIN AUFTRAG\nAnalysiere die Daten und die angeh\u00e4ngten Bilder. Nutze die Regeln des System-Prompts.\nGib das Ergebnis exakt in diesem JSON-Schema zur\u00fcck.\nWICHTIG: GIB NUR DAS NACKTE JSON AUS. KEIN MARKDOWN!\n\n{\n \"zeilen_klassifikation\": {\n \"typ\": \"NORMAL | FORTSETZUNG | SAMMELZEILE | DURCHGESTRICHEN | IGNORIEREN\",\n \"begruendung\": \"Warum hast du so entschieden?\",\n \"visuelle_marker\": [\"ROT\", \"DITO\", \"STREICHUNG\"]\n },\n \"eintrag_durchgestrichen\": false,\n \"row_id\": \"{{ $('SET_Input_Row').item.json._id }}\",\n \"_debug_llm1\": {\n \"fehler_flags\": [\n \"UNKLARE_KLASSIFIKATION\",\n \"KEIN_ANKER_GEFUNDEN\",\n \"REGELKONFLIKT_SYMBOLE\"\n ],\n \"fehler_details\": [\n \"Konflikt: ...\"\n ],\n \"unsicherheit_score\": 0.3,\n \"hinweise\": [\n \"...\"\n ]\n }\n}",
"type": "string"
},
{
"id": "c06e74dd-1e46-4c00-a165-62ba24d88fde",
"name": "rawSystempromptText",
"value": "======================================================================\nARCHIV-LLM - SYSTEM: AGNOSTISCHER KLASSIFIZIERUNGS-AGENT\n=====================================================================\n\nI. IDENTIT\u00c4T & AUFTRAG\nDu bist ein hochspezialisierter Archivar f\u00fcr die digitale Erschlie\u00dfung historischer Inventarb\u00fccher.\nDeine einzige Aufgabe ist die pr\u00e4zise Klassifizierung einer Tabellenzeile in einen der f\u00fcnf definierten Typen. Nutze daf\u00fcr die gelieferten Textdaten und die angeh\u00e4ngten Bild-Scans.\n\nII. DIE DOKTRIN DER AGNOSTIK (WICHTIG!)\nDu triffst keine Annahmen \u00fcber das Buch. Dein gesamtes Wissen \u00fcber Spalten, Rollen und Symbole beziehst du ausschlie\u00dflich aus der im User-Prompt \u00fcbergebenen 'buch_konfiguration'.\n- Identifiziere zuerst die ANKER-SPALTE: die Spalte, deren Rollenbeschreibung 'role_primaer_inventarnummer' enth\u00e4lt.\n- Identifiziere die DITO-SYMBOLE ausschlie\u00dflich aus: kontext_regeln.wiederholungs_logik.symbole.\n- Wenn in der Config zus\u00e4tzlich eine Entf\u00e4llt-/Literal-Symbol-Logik existiert (z.B. kontext_regeln.entfaellt_logik.symbole), dann sind diese Symbole als sichtbarer Inhalt zu behandeln (niemals \"leer\").\n\nIII. SYMBOL- & LEERHEITSREGELN (HART)\n1) DITO ist NUR das, was explizit in kontext_regeln.wiederholungs_logik.symbole steht.\n - Alle anderen Symbole sind NICHT DITO, auch wenn sie wie Platzhalter wirken.\n2) Sichtbarer Inhalt darf niemals als \"leer\" interpretiert werden.\n - Wenn eine Zelle irgendein Zeichen enth\u00e4lt (z.B. ein Symbol, Zahlen oder Buchstaben), ist sie NICHT leer.\n3) \"Leer\" bedeutet ausschlie\u00dflich: null oder \"\" (keine Zeichen).\n4) Keine Schema-Erfindung:\n - Ignoriere die Existenz hypothetischer zus\u00e4tzlicher Spalten (z.B. spalte_10). Klassifiziere nur anhand der vorhandenen Zellinhalte und der Config-Rollen.\n\nIV. KLASSIFIZIERUNGS-HIERARCHIE (PRIORISIERT)\nPr\u00fcfe die Zeile in exakt dieser Reihenfolge:\n\n1. DURCHGESTRICHEN (Priorit\u00e4t 1):\n - Visuelle Merkmale: Eine physische Linie verl\u00e4uft DURCH den Textk\u00f6rper \n der Anker-Spalte oder der Hauptzeile.\n - Textliche Merkmale: \"gestrichen\", \"storniert\", \"ung\u00fcltig\", \"gel\u00f6scht\" \n oder ein rotes \"X\" das den Text kreuzt.\n - PR\u00c4ZISIERUNG ROTE TINTE (KRITISCH):\n \u2192 Rote Tinte NUR dann als Streichung werten, wenn eine physische Linie\n den Textk\u00f6rper kreuzt.\n \u2192 Rote Tinte NEBEN, UNTER oder \u00dcBER dem Text ist ein Nachtrag oder\n Verwaltungsvermerk \u2013 KEIN Streichungsindikator.\n \u2192 Beispiel KEIN Streichung: roter Datumsvermerk \"3.8.70\" neben einer\n Zelle, roter Stempel, rote Kontrollnummer.\n \u2192 Beispiel STREICHUNG: rote Linie horizontal durch einen Texteintrag.\n\n2. FORTSETZUNG (Priorit\u00e4t 2):\n - Die ANKER-SPALTE ist leer (null/\"\") ODER enth\u00e4lt ausschlie\u00dflich ein DITO-SYMBOL.\n - UND es existiert in mindestens einer anderen Spalte sichtbarer, nicht-leerer Inhalt (Text, Zahl ODER ein beliebiges sichtbares Symbol).\n - Hinweis: Auch reine Symbol-/Status-/Entf\u00e4llt-Inhalte (sofern sichtbar) z\u00e4hlen als Inhalt und machen die Zeile zur FORTSETZUNG (nicht IGNORIEREN).\n\n3. SAMMELZEILE (Priorit\u00e4t 3):\n - Die ANKER-SPALTE enth\u00e4lt einen Bereich (z.B. \"XI 1-10\", \"Nr. 15 bis 20\", \"23-28\", \"XII/23\u201328\").\n - Textsignale wie \"Serie\", \"Konvolut\", \"Sammlung\", \"Konv.\", \"Gruppe\".\n\n4. NORMAL (Priorit\u00e4t 4):\n - Die ANKER-SPALTE enth\u00e4lt eine neue, valide Inventarnummer (z.B. eine valide Prim\u00e4rnummer gem\u00e4\u00df primaer_inventarnummer_regex aus der Konfiguration).\n - Auch bei OCR-Fehlern (z.B. \"X1\", \"11\", \"HI\" statt \"XI\") ist dies als NORMAL einzustufen, wenn das Muster erkennbar ist.\n - WICHTIG: Du \"reparierst\" nichts; du klassifizierst nur.\n\n5. IGNORIEREN:\n - Die Zeile ist komplett leer (alle Zellen null/\"\") oder enth\u00e4lt nur unleserliches Bildrauschen ohne interpretierbare Zeichen.\n\nV. DEBUGGING-PFLICHT (WICHTIG)\nF\u00fclle das Feld \"_debug_llm1\" NUR dann, wenn mindestens einer dieser F\u00e4lle eintritt:\n- UNKLARE_KLASSIFIKATION: Bedingungen f\u00fcr zwei oder mehr Typen sind erf\u00fcllt.\n- KEIN_ANKER_GEFUNDEN: Config liefert keine Prim\u00e4rinventarnummer-Spalte.\n- REGELKONFLIKT_SYMBOLE: DITO vs. Entf\u00e4llt vs. sichtbarer Inhalt steht in Konflikt.\nWenn alles klar ist: setze \"_debug_llm1\" auf ein leeres Objekt {}.\n\nVI. OUTPUT-FORMAT (ABSOLUTES GESETZ)\nAntworte AUSSCHLIESSLICH im puren JSON-Format gem\u00e4\u00df dem geforderten Schema.\nVerwende KEINERLEI Markdown-Formatierung (kein ```json ... ```). \nGeneriere keinen Begleittext vor oder nach dem JSON-Objekt.\n\nDOPPELKLASSIFIKATION: Eine Zeile kann gleichzeitig DURCHGESTRICHEN und FORTSETZUNG sein. Beide classification_typ-Werte d\u00fcrfen gleichzeitig gesetzt werden \u2014 z.B. classification_typ: ['DURCHGESTRICHEN', 'FORTSETZUNG']. Dies ist kein Fehler, sondern ein valider Output bei Zeilen die als Fortsetzung markiert und zus\u00e4tzlich durchgestrichen wurden.",
"type": "string"
},
{
"id": "e330cf53-5f13-4a48-b607-72052276d414",
"name": "row_scan_context",
"value": "={{ $('SET_Input_Row').item.json.row_scan_context }}",
"type": "string"
},
{
"id": "d992aa74-1812-49fd-8c1a-c1674cf772bc",
"name": "page_scan",
"value": "={{ $('SET_Input_Row').item.json.page_scan }}",
"type": "string"
},
{
"id": "bc119282-b1bd-4d2a-957a-7f0005c86ce9",
"name": "page_scan_minus_1",
"value": "={{ $('SET_Input_Row').item.json.page_scan_minus_1 }}",
"type": "string"
},
{
"id": "9fd00e7f-86a7-4171-86df-fa149bce1adc",
"name": "page_scan_minus_2",
"value": "={{ $('SET_Input_Row').item.json.page_scan_minus_2 }}",
"type": "string"
},
{
"id": "9ec316ce-f07e-463f-bfa8-60d745989137",
"name": "page_scan_minus_3",
"value": "={{ $('SET_Input_Row').item.json.page_scan_minus_3 }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2960,
496
],
"id": "d06f27d2-e462-403d-9b9b-3cff22da1472",
"name": "SET_LLM1_Prompt"
},
{
"parameters": {
"url": "={{ $json.row_json.startsWith('http') ? $json.row_json : 'https://' + $json.row_json.replace(/^[\\\\[\\\\]]/g,'') }}\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3184,
256
],
"id": "c45cb324-8cc6-43eb-a830-7301d17ac468",
"name": "HTTP_Fetch_RowJson",
"alwaysOutputData": true
},
{
"parameters": {
"url": "={{ $('SET_Input_Init').item.json.page_json.startsWith('http') ? $('SET_Input_Init').item.json.page_json : 'https://' + $('SET_Input_Init').item.json.page_json.replace(/^[\\\\[\\\\]]/g,'') }}",
"sendBody": true,
"bodyParameters": {
"parameters": [
{}
]
},
"options": {
"response": {
"response": {}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3408,
256
],
"id": "c3c250ae-b826-466b-9ab3-47e2344b02bb",
"name": "HTTP_Fetch_PageJson"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "ea74a855-51c3-4b70-bd00-c359d1dc916a",
"name": "row_json",
"value": "={{ $json.row_json }}",
"type": "string"
},
{
"id": "21efd414-eed5-41e3-b8de-1cc73be2f502",
"name": "row_scan_context",
"value": "={{ $json.row_scan_context }}",
"type": "string"
},
{
"id": "763e5362-64b9-4364-acaf-ea37c6314657",
"name": "page_scan",
"value": "={{ $json.page_scan }}",
"type": "string"
},
{
"id": "86f30f5e-d453-4dec-b012-4b3c386e718f",
"name": "page_json",
"value": "={{ $json.page_json }}",
"type": "string"
},
{
"id": "142f1f10-df23-4ae6-8192-62bc9bf98520",
"name": "year_span",
"value": "={{ $json.year_span }}",
"type": "string"
},
{
"id": "6bedfacb-b805-4166-b1ba-152bf22b7b38",
"name": "_id",
"value": "={{ $json._id }}",
"type": "string"
},
{
"id": "b8808dcb-ec85-429f-85f5-9de7e668de4c",
"name": "currentItem.previous_row_corrected",
"value": "={{ $json.previous_row_corrected }}",
"type": "string"
},
{
"id": "09255033-62b3-414d-9de2-6b87cbf41e43",
"name": "previous_rows_context_string",
"value": "={{ $json.previous_rows_context_string }}",
"type": "string"
},
{
"id": "5c06f4ad-d825-48a1-aed2-380dbd82ffe4",
"name": "config",
"value": "={{ $json.config }}",
"type": "string"
},
{
"id": "1a56a991-d79c-4670-afe4-3ddee8c22604",
"name": "validierung",
"value": "={{ $json.validierung_buchstaben }}",
"type": "string"
},
{
"id": "3ecc1539-1104-49de-9e19-e86149cf793b",
"name": "row_scan_magenta",
"value": "={{ $json.row_scan_magenta }}",
"type": "string"
},
{
"id": "init_schreiber_beschr",
"name": "schreiber_beschreibungen",
"value": "={{ $json.schreiber_beschreibungen }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2960,
256
],
"id": "a3b83e25-267a-486e-8dd4-3b25d73e8575",
"name": "SET_Input_Init"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
"name": "rawPromptText",
"value": "======================================================================\nUSER-INPUT: SEQUENZ-DATEN & BUCH-KONFIGURATION (HYBRID)\n=====================================================================\n\n### 1. DIE DATEN (FAKTEN)\nA) AKTUELLE ZEILE (Cleaned Data)\n- Rohtexte: {{ JSON.stringify($json['LLM 2'].cleaned_spalten) }}\n- Klassifizierung: {{ $json['LLM 1'].zeilen_klassifikation.typ }}\n- Durchgestrichen: {{ $json['LLM 1'].eintrag_durchgestrichen }}\n\nB) VORZEILE (Nur f\u00fcr Audit - NIEMALS ERBEN!)\n- Kontext: {{ JSON.stringify($json.Input.currentItem.previous_row_corrected) }}\n- InvNr Normalisiert: {{ $json.Input.currentItem.previous_row_corrected.inv_nr_normalisiert ?? null }}\n\nC) VISUELLE REFERENZ (BILD-ANALYSE)\nDie zugeh\u00f6rigen Scans (Detail-Scan der Zeile und der erweiterte Kontext-Scan) sind dieser Nachricht als direkte Bildanh\u00e4nge unten beigef\u00fcgt. Gleiche das Bild zwingend mit den OCR-Rohdaten ab. Achte dabei besonders auf historisch korrekte Leseweisen, Streichungen oder unklare Trennzeichen bei den Nummern.\n\n---\n### 1b. VORAB-BEFUND: SCHRIFT-VALIDATOR\nQuelle: Spezialisiertes Vision-System, unabh\u00e4ngig von Transkribus.\nMethode: Zeichenweiser Vergleich des Zeilenscans gegen eine\n schreiberspezifische Buchstabentafel.\n\n{{ JSON.stringify($json.Input.validierung) }}\n\nHinweis zur Nutzung:\n- BESTAETIGT = kein visueller Zweifel an diesem Zeichen\n- ZWEIFEL / STARK_ABWEICHEND = das Zeichen sieht im Bild\n anders aus als Transkribus es gelesen hat\n- Der Befund ist ein Indiz, kein Urteil \u2014 du entscheidest.\n\n\n---\n### 2. DIE BUCH-REGELN (DYNAMISCH)\n{{ $json.Input.config_sektion_zwei }}\n\nKONKRETE PARAMETER F\u00dcR DIESES BUCH:\n- Regex f\u00fcr Prim\u00e4rnummer (Gesetz): {{ JSON.parse($json.Input.config_sektion_zwei).cleaning_rules?.spalte_1?.regex_validation || \"KEINE REGEX - Nutze generische Plausibilit\u00e4t\" }}\n- Leerzeichen entfernen: {{ JSON.parse($json.Input.config_sektion_zwei).cleaning_rules?.spalte_1?.remove_spaces ? \"JA\" : \"NEIN\" }}\n- Zus\u00e4tzliche Suchmuster (aus Config): {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).sonderfaelle_und_annotationen?.kontrollnummern_muster || []) }}\n- Sekund\u00e4re Nummernkreise dieses Hauses: {{ (function() { const sys = JSON.parse($json.Input.config_sektion_zwei).sekundaer_nummernkreise || []; if (!sys.length) return 'KEINE DEFINIERTEN NUMMERNKREISE \u2014 nur Regex-Muster aus Zus\u00e4tzliche Suchmuster verwenden.'; return sys.map((s, i) => (i+1) + '. ' + s.name + ': ' + s.muster + (s.beispiel ? ' (Bsp: ' + s.beispiel + ')' : '')).join('\\n'); })() }}\n- Stempel-Check aktiv: {{ JSON.parse($json.Input.config_sektion_zwei).sonderfaelle_und_annotationen?.vermoegenskartei_stempel_check ? \"JA\" : \"NEIN\" }}\n\n---\n### 3. DEIN AUFTRAG\nA) PRIM\u00c4RNUMMER (STRIKT NACH REGEX)\n- Bestimme die ANKER-SPALTE aus der Config (Suche \"role_primaer_inventarnummer\").\n- Extrahiere den Wert.\n- Validiere gegen die oben genannte REGEX.\n- Wenn kein Match -> `normalisiert` = \"\".\n\nB) SEQUENZ (NUR AUDIT)\n- Vergleiche mit Vorzeile. Berechne Erwartungswert (+1).\n- Dokumentiere Abweichungen nur im Text.\n- NIEMALS die Nummer \u00e4ndern.\n\nC) SEKUND\u00c4RNUMMERN (FORENSIK)\n- Nutze deine INTERNE LISTE bekannter Systeme.\n- Nutze ZUS\u00c4TZLICH die \"Zus\u00e4tzliche Suchmuster\" von oben.\n- Suche in ALLEN Spalten.\n\nD) STEMPEL\n- Wenn aktiv: Suche nach \"Verm\u00f6gensverzeichniskartei\".",
"type": "string"
},
{
"id": "c06e74dd-1e46-4c00-a165-62ba24d88fde",
"name": "rawSystempromptText",
"value": "======================================================================\nARCHIV-LLM \u2014 SEQUENZ-VALIDATOR & FORENSIKER (AGNOSTISCH)\n=====================================================================\n\nI. IDENTIT\u00c4T & AUFTRAG\nDu bist der \u201eMathematische Revisor\".\n1) Prim\u00e4r-Inventarnummer extrahieren, reparieren und STRIKT nach Config-Regex validieren.\n2) Sekund\u00e4rnummern anhand deiner INTERNEN LISTE + Config-Hinweisen finden.\n\n---------------------------------------------------------------------\nII. INPUT-DOKTRIN \n- Prim\u00e4rnummer-Regeln (Regex, Reinigungsregeln): Kommen DYNAMISCH als JSON-Parameter aus dem User-Prompt.\n- Sekund\u00e4rnummer-Wissen: Kommt aus deiner INTERNEN LISTE (siehe unten V.) + Erg\u00e4nzungen im Prompt.\n- Visuelle Belege (Scans): Du erh\u00e4ltst zus\u00e4tzlich Bilder des Originaldokuments. Nutze diese zwingend als \"Ground Truth\" (Wahrheit), falls die OCR-Texte fehlerhaft, verrutscht oder unklar sind.\n\n\n---------------------------------------------------------------------\nII.b SCHRIFT-VALIDATOR INPUT\nWenn im User-Prompt ein Vorab-Befund des Schrift-Validators vorliegt:\n\n1. Identifiziere die Spalte mit \"role_primaer_inventarnummer\" aus der Config.\n2. Pr\u00fcfe ob f\u00fcr diese Spalte ein ZWEIFEL oder STARK_ABWEICHEND markiert ist.\n3. Wenn JA und Regex-Mismatch:\n Nutze zeichen_alternativ als zus\u00e4tzlichen Hinweis f\u00fcr einen\n Korrektur-Kandidaten. Erh\u00f6he confidence um max. 0.2.\n Dokumentiere in normalisierungsschritte:\n \"Schrift-Validator: [hinweis aus dem Befund]\"\n4. Wenn JA aber Regex-Match: Ignoriere den Befund \u2014 Transkribus hat recht.\n5. Wenn BESTAETIGT oder kein Befund vorhanden: Kein Einfluss.\n\n---------------------------------------------------------------------\nIII. HARTREGELN (ANTI-DRIFT)\n1) NO-INHERIT: Du \u00fcbernimmst NIEMALS die Nummer der Vorzeile.\n2) NO-GUESS: Passt die Nummer (auch nach Reparatur) NICHT zur Regex \u2192\n normalisiert = \"\" \u2014 ES SEI DENN du kannst den Fehler auf einen\n konkreten visuellen oder sequenziellen Beleg zur\u00fcckf\u00fchren.\n In diesem Fall: Kandidat mit confidence \u2264 0.3 und PRIMAER_NUMMER_UNKLAR.\n3) ANTI-DUP: Ist die Nummer IDENTISCH zur Vorzeile -> `normalisiert` = \"\" (Verdacht auf Fortsetzung).\n\n---------------------------------------------------------------------\nIV. PRIM\u00c4R-INVENTARNUMMER (EXTRAKTION & REPARATUR)\n1. Identifiziere Anker-Spalte (via Config im User-Prompt, suche nach \"role_primaer_inventarnummer\").\n2. Hole den Rohwert aus dieser Spalte.\n3. F\u00fchre VOR der Regex-Pr\u00fcfung ZWINGEND die im User-Prompt \u00fcbergebenen OCR-Reparaturen durch (siehe Parameter f\u00fcr \"INVENTARNUMMER\"):\n - Entferne Klammern (falls `remove_brackets` true).\n - Schneide historische Marker/Haken am Ende ab (siehe `strip_trailing_markers`).\n - Fixe Trennzeichen-Fehler der OCR (siehe `fix_separators`).\n - Entferne alle Leerzeichen (falls `remove_spaces` true).\n4. Pr\u00fcfe den reparierten Wert gegen die \u00fcbergebene `regex_validation`.\n - MATCH: Wert in `normalisiert` \u00fcbernehmen.\n - NO MATCH: Pr\u00fcfe zun\u00e4chst ob du aus dem Kontext einen plausiblen Kandidaten\n ableiten kannst:\n a) Steht im Bild eine Nummer die zur Regex passt und die der OCR-Rohwert\n erkl\u00e4rbar verf\u00e4lscht hat? (z.B. \"XI/1\" wurde als \"2/1\" gelesen,\n \"V1/3\" als \"Y1/3\", \"IV/5\" als \"1V/5\")\n b) Ergibt sich aus der Sequenz (Vorzeile + 1) ein Kandidat der zur Regex passt?\n Wenn JA: Setze normalisiert = Kandidat, confidence \u2264 0.3, und dokumentiere\n in normalisierungsschritte: \"OCR-Korrektur-Kandidat: [original] \u2192 [kandidat],\n visuell pr\u00fcfen\". Setze PRIMAER_NUMMER_UNKLAR in fehler_flags.\n Wenn NEIN: normalisiert = \"\".\n5. Falls die Anker-Spalte unplausibel ist: Pr\u00fcfe das angeh\u00e4ngte Bild der Zeile, um die Nummer visuell zu verifizieren.\n\n---------------------------------------------------------------------\nV. FORENSIK: SEKUND\u00c4R-INVENTARNUMMERN (AUS KONFIGURATION)\nScanne ALLE Spalten. Die hausspecifischen Nummernkreise stehen im User-Prompt\nunter \"Sekund\u00e4re Nummernkreise dieses Hauses\" \u2014 verwende ausschlie\u00dflich diese.\nKombiniere sie zus\u00e4tzlich mit den \"Zus\u00e4tzliche Suchmuster\" aus der Config.\nWenn keine Nummernkreise definiert sind: nur Regex-Muster verwenden.\n\nGib jeden Fund im Array `weitere_inventarnummern` aus.\n\n---------------------------------------------------------------------\nVI. STEMPEL-CHECK\nWenn im User-Input \"Stempel-Check aktiv: JA\" steht:\nSuche nach \"Verm\u00f6gensverzeichniskartei\" (oder Fragmenten).\n\n---------------------------------------------------------------------\nVII. OUTPUT SCHEMA (JSON)\n{\n \"inventarnummer\": {\n \"original\": \"...\",\n \"normalisiert\": \"...\",\n \"zusatz_informationen\": \"\",\n \"weitere_inventarnummern\": [\n { \"nummer\": \"...\", \"system\": \"...\", \"fundort_spalte\": \"...\" }\n ],\n \"normalisierungsschritte\": [],\n \"confidence\": 0.0,\n \"begruendung\": \"...\"\n },\n \"eintrag_durchgestrichen\": false,\n\n \"_debug_llm3\": {\n \"fehler_flags\": [\n \"PRIMAER_NUMMER_UNKLAR\",\n \"SEQUENZKONFLIKT\",\n \"SEKUNDAER_UNSICHER\",\n \"KEINE_REGEX_DEFINIERT\"\n ],\n \"fehler_details\": [\n \"Regex fehlt oder ist 'KEINE REGEX - Nutze generische Plausibilit\u00e4t'.\",\n \"Mehrere plausible Prim\u00e4rnummer-Kandidaten in der Anker-Spalte.\",\n \"Sequenzabweichung zum Erwartungswert +1.\",\n \"Sekund\u00e4rnummern-Kandidaten lassen sich keinem bekannten System eindeutig zuordnen.\"\n ],\n \"unsicherheit_score\": 0.3,\n \"hinweise\": [\n \"Bitte menschlich pr\u00fcfen: Prim\u00e4rnummer vs. Vorzeile und Regex-Konfiguration.\",\n \"Sekund\u00e4rinventarnummern nur mit Vorsicht \u00fcbernehmen.\"\n ]\n }\n}\n\n---------------------------------------------------------------------\nVIII. DEBUGGING-PFLICHT (OPTIONAL, ABER STRUKTURIERT)\n\n1) DEIN ZWEITER AUFTRAG \u2013 INVENTARNUMMER-KONFLIKTE SICHTBAR MACHEN\nNeben der Extraktion und Validierung der Inventarnummern musst du jede\ninhaltliche Unsicherheit oder jeden Regelkonflikt EXPLIZIT im Feld \"_debug_llm3\"\ndokumentieren.\n\n2) WANN \"_debug_llm3\" GEF\u00dcLLT WERDEN SOLL\nSetze passende Fehler-Flags, wenn mindestens einer der folgenden F\u00e4lle auftritt:\n\n- KEINE_REGEX_DEFINIERT\n Die Buch-Konfiguration enth\u00e4lt keine valide Regex f\u00fcr die Prim\u00e4rnummer.\n\n- PRIMAER_NUMMER_UNKLAR\n Kein Kandidat der nach Reparatur zur Regex passt (normalisiert bleibt leer),\n ODER ein OCR-Korrektur-Kandidat wurde gesetzt (confidence \u2264 0.3).\n\n- SEQUENZKONFLIKT\n Die aktuelle normalisierte Prim\u00e4rnummer weicht vom erwarteten Sequenzwert\n (+1 gegen\u00fcber der Vorzeile) ab. Du \u00e4nderst die Nummer NICHT, sondern\n dokumentierst nur.\n\n- SEKUNDAER_UNSICHER\n Gefundene Sekund\u00e4rnummern passen zwar teilweise zu Mustern, lassen sich aber\n keinem System eindeutig zuordnen oder wirken widerspr\u00fcchlich.\n\n3) FELDER IM DEBUG-OBJEKT\nWenn mindestens ein Konflikt vorliegt, f\u00fclle:\n- fehler_flags: Liste der Fehlercodes\n- fehler_details: kurze Begr\u00fcndungen mit Bezug auf Rohdaten/Config\n- unsicherheit_score: 0.0 (keine Unsicherheit) bis 1.0 (maximal)\n- hinweise: Empfehlung was ein Mensch pr\u00fcfen sollte\n\n4) WENN ALLES KLAR IST\nWenn keine Unsicherheit besteht:\n- lasse \"_debug_llm3\" weg oder setze es auf ein leeres Objekt {}.\nHalluziniere KEINE Debug-Fehler.",
"type": "string"
},
{
"id": "e330cf53-5f13-4a48-b607-72052276d414",
"name": "row_scan_context",
"value": "={{ $json.Input.row_scan_context }}",
"type": "string"
},
{
"id": "d992aa74-1812-49fd-8c1a-c1674cf772bc",
"name": "page_scan",
"value": "={{ $json.Input.page_scan }}",
"type": "string"
},
{
"id": "bc119282-b1bd-4d2a-957a-7f0005c86ce9",
"name": "page_scan_minus_1",
"value": "={{ $json.Input.page_scan_minus_1 }}",
"type": "string"
},
{
"id": "9fd00e7f-86a7-4171-86df-fa149bce1adc",
"name": "page_scan_minus_2",
"value": "={{ $json.Input.page_scan_minus_2 }}",
"type": "string"
},
{
"id": "9ec316ce-f07e-463f-bfa8-60d745989137",
"name": "page_scan_minus_3",
"value": "={{ $json.Input.page_scan_minus_3 }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2976,
1456
],
"id": "cb77f8ac-0714-4b5e-9d0d-184e40935229",
"name": "SET_LLM3_Prompt"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
"name": "rawPromptText",
"value": "======================================================================\nUSER-INPUT: EXTRAKTION & BLOCK-LOGIK (TRANSKRIBUS-FIRST)\n=====================================================================\nACHTUNG ROBOTER: Dies ist eine isolierte Einzelzeile ohne Kontext.\n1. Ignoriere, was in der Zeile davor stand.\n2. Wenn eine Zelle leer ist, schreibe zwingend: [LEER]\n3. Erfinde nichts. Dein Weltwissen ist verboten.\n\n### 1. DIE DATEN\n\nPRIM\u00c4RQUELLE \u2013 TRANSKRIBUS OCR (VERTRAUEN!):\n{{ JSON.stringify($json['LLM 2'].cleaned_spalten) }}\n\nKlassifizierung: {{ $json['LLM 1'].zeilen_klassifikation.typ }}\n\nVISION (zwei Scans als Anhang) NUR F\u00dcR:\n- Zellen die Transkribus LEER gelassen hat \u2192 visuell pr\u00fcfen\n- Symbole: \u00f7, ./., DITO-Zeichen, Entf\u00e4llt-Markierungen\n- Tintenfarbe: rot, blau, Bleistift\n- Streichungen: physische Linie DURCH den Textk\u00f6rper\n- Rote Nachtr\u00e4ge die Transkribus nicht erfasst hat\n\nNIEMALS: Transkribus-Text durch visuellen Eindruck oder\nWeltwissen ersetzen. \"Adviun\" bleibt \"Adviun\".\n\"Lamberger\" bleibt \"Lamberger\".\n\nDie Scans:\n- Detail-Scan (enger Ausschnitt): f\u00fcr Symbole, Farben, Streichungen\n- Kontext-Scan (mehr Padding): f\u00fcr DITO-Erkennung, kleine Zeichen\n\n---\n\n### 2. DEINE REGELN (AUS DER CONFIG)\n{{ $json.Input.config_sektion_zwei }}\n\n- Tintenfarben: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.tintenfarben || {}) }}\n- Entf\u00e4llt-Symbole: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.entfaellt_logik?.symbole || []) }}\n- DITO-Symbole: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.wiederholungs_logik?.symbole || []) }}\n\nA) DITO-LOGIK (Wiederholungen)\n- Marker: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.wiederholungs_logik?.marker || \"[DITO]\" }}\n- Symbole: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.wiederholungs_logik?.symbole || []) }}\n\nB) BLOCK-LOGIK (Mustererkennung)\n- Aktiv: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.aktiv ? \"JA\" : \"NEIN\" }}\n- Quell-Spalte: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.quelle_spalte || \"spalte_2\" }}\n- Start-Muster: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.startmuster_hinweise || []) }}\n- Fortsetzung: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.fortsetzungsmuster_hinweise || []) }}\n\n\u26a0\ufe0f HALLUZINATIONS-SPERRE F\u00dcR DIE QUELL-SPALTE (PFLICHT):\nDie Quell-Spalte lautet: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.quelle_spalte || \"spalte_2\" }}\nDie Motiv-/Darstellungs-Spalte lautet: {{ \n (() => {\n const cfg = JSON.parse($json.Input.config_sektion_zwei);\n const z = cfg.spalten_zuweisung_aktiv || cfg.spalten_zuweisung || {};\n return Object.keys(z).find(k => (z[k] || \"\").includes(\"role_darstellung_motiv\")) || \"UNBEKANNT\";\n })() \n}}\n\nREGEL: Wenn die Quell-Spalte im Scan LEER ist (keine Tinte):\n \u2192 final-Wert der Quell-Spalte = \"[LEER]\"\n \u2192 block_protokoll.event = \"EMPTY\"\n \u2192 NIEMALS \"START\" setzen, auch wenn die Motiv-Spalte einen\n Personennamen enth\u00e4lt.\n\nC) REINIGUNGS-GEBOTE (STRENG)\n- Entf\u00e4llt-Symbole (Nicht l\u00f6schen!): {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.entfaellt_logik?.symbole || []) }}\n- Rote Tinte: IST INHALT! Nicht l\u00f6schen.\n- Leerraum-Doktrin: Wenn visuell leer \u2192 [LEER]. Niemals \"\".\n\nD) SPALTEN-ROLLEN (DYNAMISCH)\nDie Motiv/Titel-Spalte (Adressen-Falle):\n{{ \n (() => {\n const cfg = JSON.parse($json.Input.config_sektion_zwei);\n const z = cfg.spalten_zuweisung_aktiv || cfg.spalten_zuweisung || {};\n return Object.keys(z).find(k => (z[k] || \"\").includes(\"role_darstellung_motiv\")) || \"KEINE_TITEL_SPALTE_DEFINIERT\";\n })() \n}}\n\n---\n\n### 3. DEIN AUFTRAG\n\nA) EXTRAKTION\n- Transkribus-Text \u00fcbernehmen wo vorhanden\n- Vision nur f\u00fcr leere Transkribus-Zellen, Symbole, Farben, Streichungen\n- Wenn Transkribus leer + Vision leer \u2192 [LEER]\n- Wenn Transkribus leer + Vision zeigt Symbol \u2192 Symbol abschreiben\n- Wenn Transkribus Text hat \u2192 Transkribus-Text verwenden, nicht \u00fcberschreiben\n\nB) BLOCK-PROTOKOLL (MUSTER-CHECK)\n- START: Eindeutiger Personenname in Quell-Spalte\n- CONTINUE: Adresse oder Ort in Quell-Spalte\n- EMPTY: Quell-Spalte visuell leer\n- IGNORE: Administrativer Inhalt (Nummern, Datum, X)\n- DITO: Ausschlie\u00dflich DITO-Symbol in Quell-Spalte\n\nC) ADRESSEN-SCHUTZ\nAdresse in Motiv-Spalte \u2192 dort lassen, nicht verschieben.\n\n---\n\n### 4. OUTPUT SCHEMA (JSON)\n{\n \"_meta_audit\": { \n \"system_modus\": \"OCR-ROBOTER V26.0\",\n \"bild_id\": \"...\"\n },\n \"spalten_vollstaendig_final\": {\n \"spalte_1\": \"...\",\n \"spalte_2\": \"...\",\n \"spalte_3\": \"...\",\n \"spalte_4\": \"...\",\n \"spalte_5\": \"...\",\n \"spalte_6\": \"...\",\n \"spalte_7\": \"...\",\n \"spalte_8\": \"...\",\n \"spalte_9\": \"...\",\n \"spalte_10\": \"...\"\n },\n \"block_protokoll\": { \n \"event\": \"START | CONTINUE | EMPTY | IGNORE | DITO\",\n \"quelle_spalte\": \"spalte_2\",\n \"begruendung\": \"Warum dieses Event?\"\n },\n \"eintrag_metadata\": {},\n \"objekt_beschreibung\": {},\n \"verwaltung_intern\": {},\n \"provenienz_intern\": {},\n \"_debug_llm4\": {\n \"fehler_flags\": [],\n \"fehler_details\": [],\n \"unsicherheit_score\": 0.0,\n \"hinweise\": []\n }\n}",
"type": "string"
},
{
"id": "c06e74dd-1e46-4c00-a165-62ba24d88fde",
"name": "rawSystempromptText",
"value": "======================================================================\nSYSTEM-MODUS: OCR-ROBOTER V26.1 (TRANSKRIBUS-FIRST + ANTI-OVERRIDE)\n=====================================================================\nDU BIST KEIN ASSISTENT. DU BIST EIN STRUKTUR-EXTRAKTOR.\nDu hast kein Ged\u00e4chtnis. Du kennst keine Vorzeilen.\n\nDEINE EINZIGE AUFGABE:\n\u00dcbernehme den Transkribus-OCR-Text aus den cleaned_spalten.\nNutze Vision ausschlie\u00dflich als Erg\u00e4nzung f\u00fcr L\u00fccken und Symbole.\n\n---------------------------------------------------------------------\nDIE 5 EISERNEN GESETZE\n\nGESETZ 1: TRANSKRIBUS IST PRIM\u00c4R\n- Der cleaned_spalten Text aus Schritt 2 ist deine Hauptquelle.\n- Du \u00fcberschreibst ihn NIEMALS mit deinem Weltwissen.\n- \"Adviun\" bleibt \"Adviun\". \"Lamberger\" bleibt \"Lamberger\".\n- \"B\u00e4derff\" bleibt \"B\u00e4derff\". \"B\u00e4cker\" ist verboten.\n- Deine Aufgabe ist Struktur und Erg\u00e4nzung, nicht Korrektur.\n\nGESETZ 2: ANTI-OVERRIDE (ABSOLUT \u2013 KEINE AUSNAHMEN)\nDie Transkribus-Priorit\u00e4t gilt AUSNAHMSLOS.\nEs gibt KEINEN Fall in dem du einen Transkribus-Textwert\ndurch visuelle Erkennung ersetzen darfst.\n- Auch wenn der Scan dir etwas anderes zu zeigen scheint.\n- Auch wenn du dir sicher bist.\n- Auch wenn du es im Debug dokumentierst.\n- Auch wenn du glaubst, die Korrektur w\u00e4re offensichtlich.\n\"Adviun\" bleibt \"Adviun\". \"B\u00e4derff\" bleibt \"B\u00e4derff\".\nDas Modell nach dir korrigiert OCR-Fehler \u2013 du nicht.\nJeden versuchten Override im Debug zu dokumentieren\nund trotzdem durchzuf\u00fchren ist ein REGELVERSTOSS.\n\nGESETZ 3: VISION ERG\u00c4NZT NUR\n- Nutze die Scans NUR f\u00fcr:\n \u2192 Zellen die Transkribus LEER gelassen hat\n \u2192 Symbole: \u00f7, ./., DITO-Zeichen, Entf\u00e4llt-Markierungen\n \u2192 Tintenfarbe: rot, blau, Bleistift\n \u2192 Streichungen: physische Linie DURCH den Textk\u00f6rper\n \u2192 Rote Nachtr\u00e4ge die Transkribus nicht erfasst hat\n- NIEMALS: Einen Transkribus-Textwert durch visuellen\n Eindruck ersetzen.\n\nGESETZ 4: TINTE IST GESETZ (AUCH ROTE!)\n- Rote Schrift, Stempel, Nachtr\u00e4ge sind DATEN. Nicht l\u00f6schen.\n- ROTE-TINTE-PR\u00c4ZISIERUNG (KRITISCH):\n \u2192 STREICHUNG: Eine Linie verl\u00e4uft physisch DURCH den Textk\u00f6rper.\n Nur dann: eintrag_durchgestrichen = true.\n Text trotzdem abschreiben.\n Block-Event NICHT automatisch EMPTY setzen.\n \u2192 NACHTRAG: Rote Tinte steht NEBEN, UNTER oder \u00dcBER dem Text.\n Als sichtbaren Inhalt erfassen.\n Kein Einfluss auf Durchgestrichen-Status.\n Kein Einfluss auf Block-Event.\n \u2192 REGEL: Nur eine physische Linie durch den Text = Streichung.\n Rote Tinte in der N\u00e4he = kein Beweis f\u00fcr Streichung.\n\nGESETZ 5: WEISS IST [LEER]\n- Wenn Transkribus leer UND Vision leer \u2192 \"[LEER]\"\n- Wenn Transkribus leer UND Vision zeigt Symbol \u2192 Symbol abschreiben\n- Niemals leeren String \"\" oder null schreiben.\n\nGESETZ 6: SPALTEN-ISOLATION\n- Jede Spalte enth\u00e4lt NUR was physisch dort steht.\n- Quell-Spalte leer \u2192 Block-Event = \"EMPTY\". Kein START.\n- Motiv-Spalte: Personennamen dort sind Bildmotive, keine Entit\u00e4ten.\n\n---------------------------------------------------------------------\nDITO-ERKENNUNG (KRITISCH)\nSchreibe [DITO] wenn du eines dieser Zeichen siehst:\n- \" \u201e \u3003 (klassische Anf\u00fchrungszeichen / Ditto-Marks)\n- Einen kleinen Strich, Haken oder H\u00e4kchen das kein Buchstabe ist\n- Ein isoliertes Zeichen das wie eine 4, ein v oder \u2193 aussieht\n- JEDES Zeichen in einer Zelle das kein vollst\u00e4ndiges Wort ist\n und nicht als Zahl oder bekanntes Symbol erkennbar ist\nWICHTIG: Wenn Transkribus die Zelle leer l\u00e4sst aber du visuell\nein kleines Zeichen siehst \u2192 pr\u00fcfe ob es ein DITO-Zeichen ist.\n\n---------------------------------------------------------------------\nFORMATIERUNG\n- DITO-Zeichen \u2192 \"[DITO]\"\n- Durchgestrichener Text: abschreiben, Status notieren.\n- Nur JSON. Keine Erkl\u00e4rungen.\n\n---------------------------------------------------------------------\nDEBUGGING-PFLICHT\nFehler-Flags wenn n\u00f6tig:\n- OCR_UNKLAR, BLOCKEVENT_UNSICHER, DITO_KONFLIKT,\n ROTE_TINTE_KONFLIKT\nWenn alles klar: _debug_llm4 weglassen oder {}.\nVERBOTEN: Einen Override dokumentieren und trotzdem ausf\u00fchren.",
"type": "string"
},
{
"id": "e330cf53-5f13-4a48-b607-72052276d414",
"name": "row_scan_context",
"value": "={{ $json.Input.row_scan_context }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2960,
1888
],
"id": "b5250efe-4691-4d31-aa19-2327ad1dcec8",
"name": "SET_LLM4_Prompt"
},
{
"parameters": {
"jsCode": "// \u2550\u2550\u2550 FN_Entity_Split | v1.0 \u2550\u2550\u2550\n// Zweck: Fan-out: GLiNER2-Entit\u00e4ten \u2192 ein Item pro Entit\u00e4t\n// Schreiber-Extraktion liest aus der SeaTable-Spalte; row_scan_magenta ist die Quelle f\u00fcr die Bildreferenz\n\nconst allOutputItems = [];\n\nfunction pick(...objs) {\n for (const o of objs) {\n if (o !== undefined && o !== null) return o;\n }\n return undefined;\n}\n\n// \u2550\u2550\u2550 Schreiber-Verwechslungspaare extrahieren (aus Flow 00b Profil) \u2550\u2550\u2550\n// Nur die eine VERWECHSLUNGSPAARE-Zeile \u2014 nicht alle ~38 Beschreibungen.\n// Quelle: SeaTable-Spalte schreiber_beschreibungen (bef\u00fcllt durch Flow 00b)\nlet schreiberVerwechslungspaare = '';\ntry {\n const inp = ($input.first().json || {}).Input || {};\n\n // SeaTable-Spalte schreiber_beschreibungen (bef\u00fcllt durch Flow 00b)\n let arr = [];\n const direct = inp.schreiber_beschreibungen;\n if (Array.isArray(direct)) {\n arr = direct;\n } else if (typeof direct === 'string' && direct.trim()) {\n try {\n const p = JSON.parse(direct);\n arr = Array.isArray(p) ? p : [direct];\n } catch (e) { arr = [direct]; }\n }\n\n // Kein Config-Fallback mehr: schreiber_beschreibungen kommt ausschlie\u00dflich aus SeaTable (Flow 00b)\n\n const vp = arr.find(s => typeof s === 'string' && s.startsWith('VERWECHSLUNGSPAARE'));\n schreiberVerwechslungspaare = vp || '';\n} catch (e) { /* Profil fehlt oder unparsebar \u2192 leer, Prompt-Fallback greift */ }\n\nconst gndTypeMapping = {\n 'person': 'Person',\n 'ort': 'PlaceOrGeographicName',\n 'institution': 'CorporateBody',\n 'sonstige': 'SubjectHeading'\n};\n\nfor (const item of items) {\n try {\n const inputJson = item.json || {};\n\n // Magenta-Bild (mit Spaltenhilfslinien) \u2014 prim\u00e4re Bildreferenz\n const rowScanMagenta = pick(\n inputJson.Input?.row_scan_magenta,\n inputJson.row_scan_magenta\n ) || '';\n\n const validierung = pick(inputJson.validierung, inputJson.Input?.validierung) || null;\n const yearSpan = pick(inputJson.Input?.year_span, inputJson.year_span) || null;\n\n // Row-ID aus _id oder aus URL ableiten\n let rowId = inputJson._id || null;\n if (!rowId && rowScanMagenta && typeof rowScanMagenta === 'string') {\n try {\n const parts = rowScanMagenta.split('/');\n rowId = parts[parts.length - 1].replace(/_(compact_enhanced|magenta)\\.(jpg|png)$/i, '');\n } catch(e) {}\n }\n\n // LLM4 Daten\n const llm4 = pick(\n inputJson[\"'LLM 4'\"],\n inputJson[\"LLM 4\"],\n inputJson[\"LLM4\"]\n ) || {};\n\n const cleanedSpalten = llm4.spalten_vollstaendig_final || {};\n\n // Entit\u00e4ten aus GLiNER2\n let entitaeten = pick(llm4.entitaeten_array, llm4.entitaeten) || [];\n if (!Array.isArray(entitaeten)) entitaeten = [];\n\n // FAN-OUT\n if (entitaeten.length > 0) {\n entitaeten.forEach((entity, index) => {\n\n const rawType = pick(entity.typ, entity.type) || 'person';\n const mappedType = gndTypeMapping[rawType.toLowerCase()] || 'Person';\n const score = entity.score || 0.9;\n const safeId = `${entity.entitaet_id || 'generated_' + index}_${index}`;\n\n allOutputItems.push({ json: {\n entitaet_id: safeId,\n entitaet_original_id: entity.entitaet_id,\n schreiber_verwechslungspaare: schreiberVerwechslungspaare,\n parent_row_id: rowId,\n row_scan_magenta: rowScanMagenta,\n year_span: yearSpan,\n\n entitaet_typ_mapped: mappedType,\n gnd_type_filter: mappedType,\n\n entitaet_name_original: entity.name_original,\n entitaet_name_norm: entity.name_norm || entity.name_original,\n entitaet_name_display: entity.name_norm || entity.name_original,\n\n // GLiNER2 liefert diese Felder nicht \u2014 Subflow 03 f\u00fcllt sie\n entitaet_untertyp: null,\n entitaet_rolle: null,\n entitaet_beruf: null,\n entitaet_lebensdaten: null,\n entitaet_anmerkung: null,\n\n entitaet_quelle_spalte: entity.quelle_spalte || null,\n entitaet_quelle_spalte_text: entity.original_context || '',\n entitaet_gliner2_score: score,\n\n cleaned_spalten: cleanedSpalten,\n validierung: validierung\n }});\n });\n\n } else {\n allOutputItems.push({ json: {\n entitaet_id: 'NO_ENTITY',\n row_scan_magenta: rowScanMagenta,\n cleaned_spalten: cleanedSpalten\n }});\n }\n\n } catch(e) {\n allOutputItems.push({ json: {\n error: e.message,\n itemIndex: items.indexOf(item)\n }});\n }\n}\n\nreturn allOutputItems;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2960,
2944
],
"id": "f04f048c-ccdd-4827-9b67-4b2c3bc4438e",
"name": "FN_Entity_Split",
"alwaysOutputData": false
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "f68a36b8-7abf-4390-a68b-122f21d48f86",
"name": "Input",
"value": "={{ $('SET_Input_Row').item.json }}",
"type": "object"
},
{
"id": "ea21d02d-442b-491c-80d3-a7591669e4d8",
"name": "LLM1_raw",
"value": "={{ $json.choices[0].message.content }}",
"type": "string"
},
{
"id": "pass_b64_ctx",
"name": "_context_scan_data_url",
"value": "={{ $('FN_Load_Images').item.json._context_scan_data_url }}",
"type": "string"
},
{
"id": "pass_b64_magenta_set_llm1_raw",
"name": "_row_scan_magenta_data_url",
"value": "={{ $('FN_Load_Images').item.json._row_scan_magenta_data_url }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
3632,
496
],
"id": "19a52fa6-8618-4b98-8dd1-8b3593f94e57",
"name": "SET_LLM1_Raw"
},
{
"parameters": {
"jsCode": "// \u2550\u2550\u2550 PARSE_LLM1 | v1.0 \u2550\u2550\u2550\n// Zweck: Parst LLM1-JSON (robust) + Fehler-Tagging\n\n/**\n * CODE 1: PARSER F\u00dcR LLM 1 (Klassifikation) - Llama.cpp Edition\n * --------------------------------------
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.
seaTableApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
02_LLM_Pipeline v1.0. Uses executeWorkflowTrigger, httpRequest, seaTable. Event-driven trigger; 65 nodes.
Source: https://github.com/sebastianruffberlin/museum-inventory-reader/blob/96500b26b10d6104116c4e5547e023bf45ea2850/n8n/02_LLM_Pipeline.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.
local_02_V1.4. Uses seaTable, executeWorkflowTrigger, httpRequest. Event-driven trigger; 31 nodes.
This template is a powerful, reusable utility for managing stateful, long-running processes. It allows a main workflow to be paused indefinitely at "checkpoints" and then be resumed by external, async
Upload files from any source to your account Kommo or AmoCRM with a simple and reusable workflow. It can split a large file into small ones and upload chunks. Works for Kommo and amoCRM There are 3 re
Remixed Backup your workflows to GitHub from Solomon's work. Check out his templates.
Remixed Backup your workflows to GitHub from Solomon's work. Check out his templates.