2026-03-17 09:31:03 +01:00
{
"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 ,
2026-03-17 17:27:49 +01:00
"position" : [ 250 , 300 ] ,
2026-03-17 09:31:03 +01:00
"parameters" : {
"cronExpression" : "*/2 * * * *"
}
} ,
{
"id" : "uuid-get-approved" ,
"name" : "Get Approved Conversations" ,
"type" : "n8n-nodes-base.httpRequest" ,
"typeVersion" : 4 ,
2026-03-17 17:27:49 +01:00
"position" : [ 450 , 300 ] ,
2026-03-17 09:31:03 +01:00
"parameters" : {
"url" : "http://host.docker.internal:4000/query/freescout" ,
"method" : "POST" ,
2026-03-17 17:27:49 +01:00
"headers" : { "Content-Type" : "application/json" } ,
2026-03-17 09:31:03 +01:00
"sendBody" : true ,
"specifyBody" : "json" ,
2026-03-17 17:27:49 +01:00
"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\"}"
2026-03-17 09:31:03 +01:00
}
} ,
{
2026-03-17 17:27:49 +01:00
"id" : "uuid-check-empty" ,
"name" : "Any Approved?" ,
"type" : "n8n-nodes-base.if" ,
"typeVersion" : 2 ,
"position" : [ 650 , 300 ] ,
2026-03-17 09:31:03 +01:00
"parameters" : {
2026-03-17 17:27:49 +01:00
"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"
}
2026-03-17 09:31:03 +01:00
}
} ,
{
2026-03-17 17:27:49 +01:00
"id" : "uuid-split-approved" ,
"name" : "Split into Items" ,
"type" : "n8n-nodes-base.splitOut" ,
"typeVersion" : 1 ,
2026-03-17 09:31:03 +01:00
"position" : [ 850 , 200 ] ,
"parameters" : {
2026-03-17 17:27:49 +01:00
"fieldToSplitOut" : "data" ,
"options" : { }
2026-03-17 09:31:03 +01:00
}
} ,
{
2026-03-17 17:27:49 +01:00
"id" : "uuid-parse-suggestion" ,
"name" : "Parse Suggestion" ,
"type" : "n8n-nodes-base.code" ,
"typeVersion" : 2 ,
2026-03-17 09:31:03 +01:00
"position" : [ 1050 , 200 ] ,
"parameters" : {
2026-03-17 17:27:49 +01:00
"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"
}
2026-03-17 09:31:03 +01:00
}
} ,
{
"id" : "uuid-execute-baramundi" ,
"name" : "Execute Baramundi Job" ,
"type" : "n8n-nodes-base.httpRequest" ,
"typeVersion" : 4 ,
2026-03-17 17:27:49 +01:00
"position" : [ 1450 , 100 ] ,
2026-03-17 09:31:03 +01:00
"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" ,
2026-03-17 17:27:49 +01:00
"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 → </p><p>\n// Nummerierten Listen erkennen und als <ol><li> ausgeben\nconst paragraphs = clean.split(/\\n{2,}/);\nconst htmlParts = paragraphs.map(para => {\n const lines = para.split('\\n');\n // Prüfen ob alle nicht-leeren Zeilen nummerierte Listeneinträge sind\n const listItems = lines.filter(l => l.trim()).every(l => /^\\d+\\.\\s/.test(l.trim()));\n if (listItems) {\n const items = lines.filter(l => l.trim()).map(l => {\n const text = l.trim().replace(/^\\d+\\.\\s*/, '');\n return '<li style=\"margin-bottom:6px\">' + text + '</li>';\n }).join('');\n return '<ol style=\"padding-left:20px;margin:8px 0\">' + items + '</ol>';\n }\n // Einzelne Zeilenumbrüche innerhalb eines Absatzes → <br>\n return '<p style=\"margin:0 0 12px 0\">' + lines.join('<br>') + '</p>';\n}).join('');\n\n// 3. Plain-Text für Freescout-Logging (Newlines erhalten)\nconst plainText = clean;\n\n// 4. HTML-E-Mail Template (Freescout-Stil: blau-grau, professionell)\nconst emailHtml = `<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head><body style=\"margin:0;padding:0;background:#f0f2f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif\"><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background:#f0f2f5;padding:32px 16px\"><tr><td align=\"center\"><table width=\"600\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,0.08)\"><tr><td style=\"background:#1b6ca8;padding:20px 32px\"><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr><td><span style=\"color:#fff;font-size:20px;font-weight:700;letter-spacing:-0.5px\">EKS InTec</span> <span style=\"color:rgba(255,255,255,0.7);font-size:14px\">IT-Support</span></td><td align=\"right\"><span style=\"background:rgba(255,255,255,0.15);color:#fff;font-size:12px;padding:4px 10px;border-radius:12px\">Ticket #${p.ticket_number}</span></td></tr></table></td></tr><tr><td style=\"padding:32px 32px 8px;color:#1a1a1a;font-size:15px;line-height:1.6\">${htmlParts}</td></tr><tr><td style=\"padding:16px 32px 32px\"><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr><td style=\"border-top:1px solid #e8eaed;padding-top:16px;font-size:13px;color:#5f6368\">Diese Antwort wurde automatisch durch das IT-Support-System erstellt.<br>Bei weiteren Fragen antworten Sie einfach auf diese E-Mail.</td></tr></table></td></tr><tr><td style=\"background:#f8f9fa;border-top:1px solid #e8eaed;padding:16px 32px\"><table width=\"100%\"><tr><td style=\"font-size:12px;color:#9aa0a6\">EKS InTec GmbH • IT-Support • <a href=\"mailto:it@eks-intec.de\" style=\"color:#1b6ca8;text-decoration:none\" > i t @ e k s - i n t e c . d e < / a > < / t d > < / t r > < / t a b l e > < / t d > < / t r > < / t a b l e > < / t d > < / t r > < / t a b l e > < / b o d y > < / h t m l > ` ; \ n \ n r e t u r n { j s o n : { \ n t i c k e t _ i d : p . t i c k e t _ i d , \ n t i c k e t _ n u m b e r : p . t i c k e t _ n u m b e r , \ n s u b j e c t : p . s u b j e c t , \ n c u s t o m e r _ e m a i l : p . c u s t o m e r _ e m a i l , \ n l o e
2026-03-17 09:31:03 +01:00
}
} ,
{
2026-03-17 17:27:49 +01:00
"id" : "uuid-send-reply" ,
2026-03-17 09:31:03 +01:00
"name" : "Send Email Reply" ,
2026-03-17 17:27:49 +01:00
"type" : "n8n-nodes-base.emailSend" ,
"typeVersion" : 1 ,
"position" : [ 1650 , 200 ] ,
2026-03-17 09:31:03 +01:00
"parameters" : {
2026-03-17 17:27:49 +01:00
"fromEmail" : "it@eks-intec.de" ,
"toEmail" : "={{ $json.email_to }}" ,
"subject" : "={{ $json.email_subject }}" ,
"html" : "={{ $json.email_html }}" ,
"text" : "={{ $json.email_body }}" ,
"options" : { }
2026-03-17 09:31:03 +01:00
}
} ,
{
2026-03-17 17:27:49 +01:00
"id" : "uuid-log-freescout-reply" ,
"name" : "Log Reply to Freescout" ,
"type" : "n8n-nodes-base.code" ,
"typeVersion" : 2 ,
"position" : [ 1850 , 200 ] ,
2026-03-17 09:31:03 +01:00
"parameters" : {
2026-03-17 17:27:49 +01:00
"mode" : "runOnceForEachItem" ,
"jsCode" : "const parsed = $('Parse Suggestion').item.json;\nconst emailData = $('Prepare Email Body').item.json;\nconst ticketId = parsed.ticket_id;\n// Vollständigen E-Mail-Text in Freescout eintragen (als Agent Reply sichtbar)\n// type=2 = Agent Reply (Freescout: 1=Customer, 2=Message/Agent, 3=Note, 4=LineItem)\n// type=0 existiert NICHT -> Thread.php:420 crasht\n// Zeilenumbrüche -> <br> damit Freescout sie korrekt anzeigt, Quotes escapen\nconst rawBody = (emailData.email_body || 'Automatische Antwort gesendet.')\n .replace(/'/g, \"''\")\n .replace(/\\n/g, '<br>\\n');\n// created_at = MAX(existierende Threads) + 1 Sekunde, damit unser Reply\n// immer als neuester Thread erscheint (Freescout zeigt neueste oben).\n// Hintergrund: Kunden-Emails haben created_at aus dem Mail-Header-Datum,\n// das häufig in der Zukunft liegt relativ zum Zeitpunkt unserer Verarbeitung.\nconst query = \"INSERT INTO threads (conversation_id, type, status, state, body, created_by_user_id, user_id, customer_id, source_via, source_type, cc, bcc, created_at, updated_at) SELECT \" + ticketId + \", 2, 1, 2, '\" + rawBody + \"', 1, 1, customer_id, 1, 1, '[]', '[]', GREATEST(NOW(), IFNULL((SELECT MAX(t2.created_at) FROM threads t2 WHERE t2.conversation_id = \" + ticketId + \"), NOW())) + INTERVAL 1 SECOND, NOW() FROM conversations WHERE id = \" + ticketId;\nreturn { json: { query, ticket_id: ticketId } };"
2026-03-17 09:31:03 +01:00
}
} ,
{
2026-03-17 17:27:49 +01:00
"id" : "uuid-write-freescout-thread" ,
"name" : "Write Thread to Freescout DB" ,
2026-03-17 09:31:03 +01:00
"type" : "n8n-nodes-base.httpRequest" ,
"typeVersion" : 4 ,
2026-03-17 17:27:49 +01:00
"position" : [ 2050 , 200 ] ,
2026-03-17 09:31:03 +01:00
"parameters" : {
"url" : "http://host.docker.internal:4000/query/freescout" ,
"method" : "POST" ,
2026-03-17 17:27:49 +01:00
"headers" : { "Content-Type" : "application/json" } ,
2026-03-17 09:31:03 +01:00
"sendBody" : true ,
"specifyBody" : "json" ,
2026-03-17 17:27:49 +01:00
"jsonBody" : "={{ JSON.stringify({ query: $json.query }) }}"
}
} ,
{
"id" : "uuid-mark-escalation" ,
"name" : "Mark Escalation" ,
"type" : "n8n-nodes-base.code" ,
"typeVersion" : 2 ,
"position" : [ 1650 , 400 ] ,
"parameters" : {
"mode" : "runOnceForEachItem" ,
"jsCode" : "return { json: { ...$input.item.json, action: 'ESKALATION - manuelle Bearbeitung erforderlich' } };"
2026-03-17 09:31:03 +01:00
}
} ,
{
2026-03-17 17:27:49 +01:00
"id" : "uuid-update-executed" ,
"name" : "Update Status to EXECUTED" ,
2026-03-17 09:31:03 +01:00
"type" : "n8n-nodes-base.httpRequest" ,
"typeVersion" : 4 ,
2026-03-17 17:27:49 +01:00
"position" : [ 2250 , 200 ] ,
2026-03-17 09:31:03 +01:00
"parameters" : {
2026-03-17 17:27:49 +01:00
"url" : "http://host.docker.internal:4000/query/freescout" ,
2026-03-17 09:31:03 +01:00
"method" : "POST" ,
2026-03-17 17:27:49 +01:00
"headers" : { "Content-Type" : "application/json" } ,
2026-03-17 09:31:03 +01:00
"sendBody" : true ,
"specifyBody" : "json" ,
2026-03-17 17:27:49 +01:00
"jsonBody" : "={{ JSON.stringify({query: \"UPDATE conversation_custom_field SET value = '3' WHERE conversation_id = \" + $('Parse Suggestion').item.json.ticket_id + \" AND custom_field_id = 7\"}) }}"
2026-03-17 09:31:03 +01:00
}
} ,
{
"id" : "uuid-log-audit" ,
"name" : "Log to PostgreSQL" ,
"type" : "n8n-nodes-base.httpRequest" ,
"typeVersion" : 4 ,
2026-03-17 17:27:49 +01:00
"position" : [ 2450 , 200 ] ,
2026-03-17 09:31:03 +01:00
"parameters" : {
"url" : "http://host.docker.internal:4000/query/audit" ,
"method" : "POST" ,
2026-03-17 17:27:49 +01:00
"headers" : { "Content-Type" : "application/json" } ,
2026-03-17 09:31:03 +01:00
"sendBody" : true ,
"specifyBody" : "json" ,
2026-03-17 17:27:49 +01:00
"jsonBody" : "={{ JSON.stringify({query: \"INSERT INTO workflow_executions (workflow_name, ticket_id, status, execution_time_ms, created_at) VALUES ('Workflow B - Approval Execution', \" + $('Parse Suggestion').item.json.ticket_id + \", 'SUCCESS', 0, NOW())\"}) }}"
}
} ,
{
"id" : "uuid-no-approved" ,
"name" : "No Approved Tickets" ,
"type" : "n8n-nodes-base.code" ,
"typeVersion" : 2 ,
"position" : [ 850 , 400 ] ,
"parameters" : {
"mode" : "runOnceForEachItem" ,
"jsCode" : "return { json: { status: 'no_approved_tickets', timestamp: new Date().toISOString() } };"
2026-03-17 09:31:03 +01:00
}
}
] ,
"connections" : {
"Trigger" : {
2026-03-17 17:27:49 +01:00
"main" : [ [ { "node" : "Get Approved Conversations" , "index" : 0 } ] ]
2026-03-17 09:31:03 +01:00
} ,
"Get Approved Conversations" : {
2026-03-17 17:27:49 +01:00
"main" : [ [ { "node" : "Any Approved?" , "index" : 0 } ] ]
2026-03-17 09:31:03 +01:00
} ,
2026-03-17 17:27:49 +01:00
"Any Approved?" : {
2026-03-17 09:31:03 +01:00
"main" : [
2026-03-17 17:27:49 +01:00
[ { "node" : "Split into Items" , "index" : 0 } ] ,
[ { "node" : "No Approved Tickets" , "index" : 0 } ]
2026-03-17 09:31:03 +01:00
]
} ,
2026-03-17 17:27:49 +01:00
"Split into Items" : {
"main" : [ [ { "node" : "Parse Suggestion" , "index" : 0 } ] ]
} ,
"Parse Suggestion" : {
"main" : [ [ { "node" : "Is Baramundi Job?" , "index" : 0 } ] ]
} ,
"Is Baramundi Job?" : {
2026-03-17 09:31:03 +01:00
"main" : [
2026-03-17 17:27:49 +01:00
[ { "node" : "Execute Baramundi Job" , "index" : 0 } ] ,
[ { "node" : "Is Auto Reply?" , "index" : 0 } ]
2026-03-17 09:31:03 +01:00
]
} ,
2026-03-17 17:27:49 +01:00
"Is Auto Reply?" : {
2026-03-17 09:31:03 +01:00
"main" : [
2026-03-17 17:27:49 +01:00
[ { "node" : "Prepare Email Body" , "index" : 0 } ] ,
[ { "node" : "Mark Escalation" , "index" : 0 } ]
2026-03-17 09:31:03 +01:00
]
} ,
2026-03-17 17:27:49 +01:00
"Prepare Email Body" : {
"main" : [ [ { "node" : "Send Email Reply" , "index" : 0 } ] ]
} ,
2026-03-17 09:31:03 +01:00
"Execute Baramundi Job" : {
2026-03-17 17:27:49 +01:00
"main" : [ [ { "node" : "Update Status to EXECUTED" , "index" : 0 } ] ]
2026-03-17 09:31:03 +01:00
} ,
"Send Email Reply" : {
2026-03-17 17:27:49 +01:00
"main" : [ [ { "node" : "Log Reply to Freescout" , "index" : 0 } ] ]
2026-03-17 09:31:03 +01:00
} ,
2026-03-17 17:27:49 +01:00
"Log Reply to Freescout" : {
"main" : [ [ { "node" : "Write Thread to Freescout DB" , "index" : 0 } ] ]
2026-03-17 09:31:03 +01:00
} ,
2026-03-17 17:27:49 +01:00
"Write Thread to Freescout DB" : {
"main" : [ [ { "node" : "Update Status to EXECUTED" , "index" : 0 } ] ]
2026-03-17 09:31:03 +01:00
} ,
2026-03-17 17:27:49 +01:00
"Mark Escalation" : {
"main" : [ [ { "node" : "Update Status to EXECUTED" , "index" : 0 } ] ]
} ,
"Update Status to EXECUTED" : {
"main" : [ [ { "node" : "Log to PostgreSQL" , "index" : 0 } ] ]
2026-03-17 09:31:03 +01:00
}
} ,
"active" : false ,
"settings" : {
"errorHandler" : "continueOnError"
}
}