{ "name": "Workflow B - Approval & Execution (HTTP)", "description": "Poll for approved AI suggestions and execute them (Baramundi jobs or email replies)", "nodes": [ { "id": "uuid-trigger-b", "name": "Trigger", "type": "n8n-nodes-base.cron", "typeVersion": 1, "position": [250, 300], "parameters": { "cronExpression": "*/2 * * * *" } }, { "id": "uuid-get-approved", "name": "Get Approved Conversations", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, "position": [450, 300], "parameters": { "url": "http://host.docker.internal:4000/query/freescout", "method": "POST", "headers": { "Content-Type": "application/json" }, "sendBody": true, "specifyBody": "json", "jsonBody": "{\"query\":\"SELECT c.id as ticket_id, c.number as ticket_number, c.subject, c.customer_email, ccf6.value as ai_suggestion_raw, ccf7.value as approval_status FROM conversations c JOIN conversation_custom_field ccf7 ON c.id = ccf7.conversation_id AND ccf7.custom_field_id = 7 LEFT JOIN conversation_custom_field ccf6 ON c.id = ccf6.conversation_id AND ccf6.custom_field_id = 6 WHERE ccf7.value = '1' LIMIT 10\"}" } }, { "id": "uuid-check-empty", "name": "Any Approved?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [650, 300], "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" }, "conditions": [ { "id": "cond-has-data", "leftValue": "={{ $json.data.length }}", "rightValue": 0, "operator": { "type": "number", "operation": "gt" } } ], "combinator": "and" } } }, { "id": "uuid-split-approved", "name": "Split into Items", "type": "n8n-nodes-base.splitOut", "typeVersion": 1, "position": [850, 200], "parameters": { "fieldToSplitOut": "data", "options": {} } }, { "id": "uuid-parse-suggestion", "name": "Parse Suggestion", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1050, 200], "parameters": { "mode": "runOnceForEachItem", "jsCode": "const item = $input.item.json;\nconst ticketId = item.ticket_id;\nconst raw = item.ai_suggestion_raw || '';\n\nlet loesung_typ = 'ESKALATION';\nlet baramundi_job = '';\nlet antwort_text = '';\n\n// Lösung-Typ aus dem ersten Segment extrahieren\nconst firstPart = raw.split('|')[0].trim();\nif (firstPart === 'BARAMUNDI_JOB' || firstPart === 'AUTOMATISCHE_ANTWORT' || firstPart === 'ESKALATION') {\n loesung_typ = firstPart;\n}\n\n// Baramundi-Job Name extrahieren\nconst jobMatch = raw.match(/Baramundi-Job:\\s*([^|\\n]+)/);\nif (jobMatch) baramundi_job = jobMatch[1].trim();\n\n// Antworttext nach '--- |' extrahieren\nconst sepIdx = raw.indexOf('--- |');\nif (sepIdx !== -1) {\n antwort_text = raw.substring(sepIdx + 5).trim();\n}\n\n// Fallback: gesamter raw-Text wenn antwort_text leer\nif (!antwort_text && loesung_typ === 'AUTOMATISCHE_ANTWORT') {\n // Versuche nach '---' ohne Pipe zu suchen\n const sepIdx2 = raw.indexOf('---');\n if (sepIdx2 !== -1) {\n antwort_text = raw.substring(sepIdx2 + 3).replace(/^\\s*\\|\\s*/, '').trim();\n }\n // Letzter Fallback: alle Segmente nach dem 4. Pipe-Zeichen\n if (!antwort_text) {\n const parts = raw.split('|');\n if (parts.length > 3) {\n antwort_text = parts.slice(3).join('|').trim();\n }\n }\n}\n\nreturn { json: {\n ticket_id: ticketId,\n ticket_number: item.ticket_number,\n subject: item.subject,\n customer_email: item.customer_email,\n loesung_typ,\n baramundi_job,\n antwort_text,\n raw_suggestion: raw\n}};" } }, { "id": "uuid-is-baramundi", "name": "Is Baramundi Job?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [1250, 200], "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" }, "conditions": [ { "id": "cond-baramundi", "leftValue": "={{ $json.loesung_typ }}", "rightValue": "BARAMUNDI_JOB", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" } } }, { "id": "uuid-execute-baramundi", "name": "Execute Baramundi Job", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, "position": [1450, 100], "parameters": { "url": "https://baramundi-api.example.com/api/jobs", "method": "POST", "headers": { "Content-Type": "application/json", "Authorization": "Bearer YOUR_BARAMUNDI_TOKEN" }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({job_name: $json.baramundi_job, ticket_id: $json.ticket_id, description: $json.subject}) }}" } }, { "id": "uuid-is-auto-reply", "name": "Is Auto Reply?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [1450, 300], "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" }, "conditions": [ { "id": "cond-autoreply", "leftValue": "={{ $json.loesung_typ }}", "rightValue": "AUTOMATISCHE_ANTWORT", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" } } }, { "id": "uuid-prepare-email", "name": "Prepare Email Body", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1450, 200], "parameters": { "mode": "runOnceForEachItem", "jsCode": "const p = $('Parse Suggestion').item.json;\nconst rawBody = p.antwort_text || p.raw_suggestion || 'Bitte wenden Sie sich an den IT-Support.';\n\n// 1. Markdown entfernen + Struktur wiederherstellen\nlet clean = rawBody\n .replace(/¶/g, '\\n') // ¶-Platzhalter → echte Newlines (neue Tickets)\n .replace(/\\*\\*/g, '')\n .replace(/\\*/g, '')\n .replace(/_{2}/g, '')\n .replace(/ \\| /g, '\\n') // Pipe-Trennzeichen → Zeilenumbrüche\n // Fallback für alte Tickets (flacher Text ohne ¶): Struktur per Regex\n .replace(/ (\\d{1,2}\\.) ([A-ZÄÖÜ])/g, '\\n\\n$1 $2') // \" 1. Windows\" → Absatz\n .replace(/\\. - ([A-ZÄÖÜ])/g, '.\\n- $1') // \". - Öffnen\" → Aufzählung\n .replace(/ (Mit freundlichen)/g, '\\n\\n$1') // Grußformel\n .replace(/ (Sollten Sie)/g, '\\n\\n$1') // Abschlussatz\n .replace(/[ \\t]{2,}/g, ' ') // horizontale Mehrfach-Spaces normalisieren\n .trim();\n\n// 2. Text → HTML konvertieren\n// Absätze: doppelte Newlines →
\n// Nummerierten Listen erkennen und als
' + lines.join('
') + '