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": "Agent 2 - Loop Completo con Form HTML",
"nodes": [
{
"parameters": {
"httpMethod": "GET",
"path": "agent2-start",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-start",
"name": "START",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
400
],
"notes": "Webhook GET - mostra la pagina iniziale nel browser"
},
{
"parameters": {
"httpMethod": "POST",
"path": "agent2-submit",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-submit",
"name": "SUBMIT",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
560
],
"notes": "Webhook POST - riceve tutti i form submissions"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body || raw;\nlet requestType = 'initial';\n\nif (body.answers && body.domain_model_b64) {\n requestType = 'answers';\n} else if (body.file_name) {\n requestType = 'file';\n} else if (body.mode === 'local') {\n requestType = 'local';\n} else if (body.mode === 'a2a') {\n requestType = 'a2a';\n} else if (body.domain_model) {\n requestType = 'file';\n}\n\nreturn [{ json: { ...body, requestType } }];"
},
"id": "detect-type",
"name": "Detect Request Type",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
400
],
"notes": "Classifica la richiesta in: initial, local, file, a2a, answers"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.requestType }}",
"rightValue": "initial",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": "initial"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.requestType }}",
"rightValue": "local",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": "local"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.requestType }}",
"rightValue": "file",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": "file"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.requestType }}",
"rightValue": "a2a",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": "a2a"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.requestType }}",
"rightValue": "answers",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": "answers"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "route",
"name": "Route",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
680,
400
],
"notes": "Smista in 5 percorsi: initial, local, file, a2a, answers"
},
{
"parameters": {
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html lang=\"it\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Agent 2 - Scegli Metodo</title>\n <style>\n *{margin:0;padding:0;box-sizing:border-box}\n body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);min-height:100vh;display:flex;justify-content:center;align-items:center;padding:20px;color:#0f172a}\n .container{background:#fff;width:100%;max-width:720px;border-radius:18px;padding:48px 40px;box-shadow:0 24px 70px rgba(0,0,0,0.35);animation:fadeIn .5s}\n @keyframes fadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}\n h1{font-size:30px;margin-bottom:8px;color:#0f172a}\n .subtitle{color:#475569;margin-bottom:32px;line-height:1.6;font-size:15px}\n .actions{display:grid;gap:16px}\n .btn{padding:20px 24px;border-radius:14px;border:none;font-size:17px;font-weight:700;cursor:pointer;transition:transform .2s,box-shadow .2s;text-align:left;display:flex;align-items:center;gap:16px}\n .btn:hover{transform:translateY(-3px);box-shadow:0 12px 28px rgba(15,23,42,0.2)}\n .btn-local{background:linear-gradient(135deg,#14b8a6,#0f766e);color:#fff}\n .btn-a2a{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff}\n .btn-icon{font-size:28px;flex-shrink:0}\n .btn-text strong{display:block;margin-bottom:2px}\n .btn-text span{font-weight:400;font-size:13px;opacity:.85}\n .footer{margin-top:28px;font-size:12px;color:#94a3b8;text-align:center}\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>Agent 2 - Consistency Analyzer</h1>\n <p class=\"subtitle\">Come vuoi fornire il modello di dominio DDD da analizzare?</p>\n <div class=\"actions\">\n <button class=\"btn btn-local\" id=\"choose-local\">\n <span class=\"btn-icon\">📂</span>\n <span class=\"btn-text\"><strong>File locale</strong><span>Seleziona un file JSON dalla cartella input_agent</span></span>\n </button>\n <button class=\"btn btn-a2a\" id=\"choose-a2a\">\n <span class=\"btn-icon\">🔗</span>\n <span class=\"btn-text\"><strong>A2A Protocol</strong><span>Attendi il modello da Agent 1 via Agent-to-Agent</span></span>\n </button>\n </div>\n <div class=\"footer\">Agent 2 - Consistency & Conflict Analyzer | Powered by n8n + Ollama</div>\n </div>\n <script>\n async function postMode(mode) {\n const btn = event.currentTarget;\n btn.style.opacity = '0.6';\n btn.style.pointerEvents = 'none';\n try {\n const res = await fetch(window.location.href.replace('agent2-start', 'agent2-submit'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ mode })\n });\n document.open();\n document.write(await res.text());\n document.close();\n } catch(e) {\n alert('Errore di connessione: ' + e.message);\n btn.style.opacity = '1';\n btn.style.pointerEvents = 'auto';\n }\n }\n document.getElementById('choose-local').addEventListener('click', () => postMode('local'));\n document.getElementById('choose-a2a').addEventListener('click', () => postMode('a2a'));\n </script>\n</body>\n</html>",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
}
]
}
}
},
"id": "choice-page",
"name": "Pagina Scelta",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
920,
180
],
"notes": "Pagina iniziale: scegli tra file locale o A2A"
},
{
"parameters": {
"method": "GET",
"url": "=http://agent2-api:8002/source/files",
"options": {
"timeout": 10000
}
},
"id": "get-files",
"name": "Lista File API",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
920,
340
],
"notes": "Chiama GET /source/files per ottenere la lista dei JSON disponibili"
},
{
"parameters": {
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html lang=\"it\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Seleziona File</title>\n <style>\n *{margin:0;padding:0;box-sizing:border-box}\n body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:linear-gradient(135deg,#f0fdf4 0%,#d1fae5 100%);min-height:100vh;display:flex;justify-content:center;align-items:center;padding:20px}\n .container{background:#fff;width:100%;max-width:720px;border-radius:18px;padding:40px;box-shadow:0 24px 70px rgba(15,23,42,0.15)}\n h1{font-size:24px;margin-bottom:6px;color:#065f46}\n .subtitle{color:#475569;margin-bottom:24px;font-size:14px}\n .file-list{display:grid;gap:10px}\n .file-btn{padding:16px 20px;border-radius:12px;border:2px solid #d1fae5;background:#f0fdf4;color:#065f46;font-weight:600;font-size:15px;cursor:pointer;transition:all .2s;text-align:left;display:flex;justify-content:space-between;align-items:center}\n .file-btn:hover{background:#065f46;color:#fff;border-color:#065f46;transform:translateY(-2px);box-shadow:0 8px 20px rgba(6,95,70,0.2)}\n .file-btn .size{font-size:12px;opacity:.7;font-weight:400}\n .empty{color:#64748b;font-style:italic;padding:20px;text-align:center}\n .back{display:inline-block;margin-top:20px;color:#0f766e;text-decoration:none;font-size:13px;cursor:pointer}\n .back:hover{text-decoration:underline}\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>Seleziona un file da analizzare</h1>\n <p class=\"subtitle\">File disponibili nella cartella input_agent:</p>\n <div id=\"list\" class=\"file-list\">\n {{ $json.files ? $json.files.map(f => '<button class=\"file-btn\" data-name=\"' + f.name + '\">' + f.name + '<span class=\"size\">' + (f.size ? Math.round(f.size/1024) + ' KB' : '') + '</span></button>').join('') : '<div class=\"empty\">Nessun file trovato.</div>' }}\n </div>\n <a class=\"back\" id=\"go-back\">← Torna alla scelta</a>\n </div>\n <script>\n document.querySelectorAll('.file-btn').forEach(btn => {\n btn.addEventListener('click', async () => {\n const name = btn.dataset.name;\n // Mostra overlay di caricamento SUBITO, prima della fetch\n const overlay = document.createElement('div');\n overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,#0f172a,#1e293b);display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:9999;color:#fff;font-family:Segoe UI,sans-serif;text-align:center;padding:20px';\n overlay.innerHTML = '<div style=\"font-size:60px;margin-bottom:20px\">🔍</div><h2 style=\"margin-bottom:12px;font-size:24px\">Analisi in corso...</h2><p style=\"color:#94a3b8;margin-bottom:8px\">File: <strong style=\"color:#38bdf8\">' + name + '</strong></p><p style=\"color:#94a3b8;margin-bottom:30px;font-size:14px\">LLM sta analizzando il modello DDD.<br>Questa operazione può richiedere 1-3 minuti.</p><div style=\"display:flex;gap:10px;justify-content:center\"><div style=\"width:12px;height:12px;border-radius:50%;background:#38bdf8;animation:p 1.4s infinite\"></div><div style=\"width:12px;height:12px;border-radius:50%;background:#38bdf8;animation:p 1.4s .2s infinite\"></div><div style=\"width:12px;height:12px;border-radius:50%;background:#38bdf8;animation:p 1.4s .4s infinite\"></div></div><style>@keyframes p{0%,100%{transform:scale(.8);opacity:.5}50%{transform:scale(1.2);opacity:1}}</style>';\n document.body.appendChild(overlay);\n try {\n const res = await fetch(window.location.href.replace('agent2-start', 'agent2-submit'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ file_name: name })\n });\n const html = await res.text();\n if (!html || html.trim() === '') {\n throw new Error('Risposta vuota dal server. Verifica che Ollama sia avviato e il modello sia caricato (ollama run qwen2.5:14b).');\n }\n document.open();\n document.write(html);\n document.close();\n } catch(e) {\n document.body.removeChild(overlay);\n document.querySelectorAll('.file-btn').forEach(b => b.style.pointerEvents = 'auto');\n alert('Errore durante l\\'analisi:\\n\\n' + e.message);\n }\n });\n });\n document.getElementById('go-back').addEventListener('click', async () => {\n try {\n const res = await fetch(window.location.href.replace('agent2-start', 'agent2-submit'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({})\n });\n document.open();\n document.write(await res.text());\n document.close();\n } catch(e) { location.reload(); }\n });\n </script>\n</body>\n</html>",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
}
]
}
}
},
"id": "file-selector",
"name": "Seleziona File",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1140,
340
],
"notes": "Mostra la lista dei file come bottoni cliccabili"
},
{
"parameters": {
"method": "GET",
"url": "=http://agent2-api:8002/source/file?name={{ encodeURIComponent($json.file_name) }}",
"options": {
"timeout": 10000
}
},
"id": "load-file",
"name": "Carica File",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
920,
500
],
"notes": "Chiama GET /source/file?name=... per caricare il contenuto JSON"
},
{
"parameters": {
"jsCode": "const input = $input.first().json;\nlet domain_model, iteration, max_iterations, session_id, previous_answers, file_name;\n\n// Check if this is the 'answers' path (loop iteration)\nif (input.domain_model_b64) {\n // Decode base64 domain model from form\n const decoded = Buffer.from(input.domain_model_b64, 'base64').toString('utf-8');\n domain_model = JSON.parse(decoded);\n iteration = parseInt(input.iteration) || 0;\n max_iterations = parseInt(input.max_iterations) || 5;\n session_id = input.session_id || new Date().toISOString();\n previous_answers = input.answers || {};\n file_name = input.file_name || '';\n} else if (input.domain_model) {\n // Direct A2A submission with domain_model in body\n domain_model = typeof input.domain_model === 'string' ? JSON.parse(input.domain_model) : input.domain_model;\n iteration = 0;\n max_iterations = 5;\n session_id = new Date().toISOString();\n previous_answers = {};\n file_name = '';\n} else {\n // File loaded via HTTP - the entire response IS the domain model\n domain_model = input;\n iteration = 0;\n max_iterations = 5;\n session_id = new Date().toISOString();\n previous_answers = {};\n file_name = '';\n}\n\n// Soft cap: if we hit max iterations, extend automatically and continue.\nif (iteration >= max_iterations) {\n max_iterations = iteration + 1;\n}\n\nreturn [{\n json: {\n domain_model: JSON.stringify(domain_model),\n iteration,\n max_iterations,\n session_id,\n previous_answers: JSON.stringify(previous_answers),\n use_llm: true,\n apply_auto_fixes: true,\n file_name\n }\n}];"
},
"id": "prepare-data",
"name": "Prepara Dati",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
560
],
"notes": "Normalizza i dati per l'analisi.\nGestisce sia prima iterazione (da file) che loop (da answers con base64)."
},
{
"parameters": {
"method": "POST",
"url": "=http://agent2-api:8002/analyze",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({\n domain_model: JSON.parse($json.domain_model),\n use_llm: $json.use_llm,\n apply_auto_fixes: $json.apply_auto_fixes,\n previous_answers: JSON.parse($json.previous_answers),\n original_filename: $json.file_name || '',\n session_id: $json.session_id || '',\n iteration: $json.iteration || 0\n}) }}",
"options": {
"timeout": 180000
}
},
"id": "analyze-api",
"name": "Analizza Modello",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1580,
620
],
"notes": "POST /analyze - Analisi completa del modello DDD.\nTimeout 3 minuti per dare tempo all'LLM."
},
{
"parameters": {
"jsCode": "const apiResult = $input.first().json;\nconst prepData = $('Prepara Dati').first().json;\n\nconst questions = apiResult.follow_up_questions || [];\nconst refinedModel = apiResult.refined_model || {};\nconst domainModelStr = Object.keys(refinedModel).length > 0 ? JSON.stringify(refinedModel) : prepData.domain_model;\nconst domainModelB64 = Buffer.from(domainModelStr).toString('base64');\n\n// Adaptive max_iterations: refresh at every iteration based on latest LLM estimate.\nconst suggested = Number(apiResult.suggested_max_iterations || 0);\nconst remainingEstimate = suggested > 0 ? suggested : 3;\nconst adaptiveMax = (prepData.iteration + 1) + remainingEstimate;\nconst maxIter = Math.max(Number(prepData.max_iterations || 0), adaptiveMax);\n\n// Pre-render questions HTML\nlet qHtml = '';\nqHtml += '<input type=\"hidden\" name=\"session_id\" value=\"' + prepData.session_id + '\">';\nqHtml += '<input type=\"hidden\" name=\"iteration\" value=\"' + (prepData.iteration + 1) + '\">';\nqHtml += '<input type=\"hidden\" name=\"max_iterations\" value=\"' + maxIter + '\">';\nqHtml += '<input type=\"hidden\" id=\"domain_model_b64\" name=\"domain_model_b64\" value=\"' + domainModelB64 + '\">';\nqHtml += '<input type=\"hidden\" id=\"question_count\" value=\"' + questions.length + '\">';\nqHtml += '<input type=\"hidden\" name=\"file_name\" value=\"' + (prepData.file_name || '') + '\">';\nquestions.forEach((q, i) => {\n const sev = q.severity || 'MEDIUM';\n qHtml += '<div class=\"q-block\">';\n qHtml += '<div class=\"q-num\">' + (i+1) + '</div>';\n qHtml += '<div class=\"q-label\">' + (q.question || '') + ' <span class=\"severity sev-' + sev + '\">' + sev + '</span></div>';\n if (q.context) qHtml += '<div class=\"context-info\">' + q.context + '</div>';\n if (q.suggested_answers && q.suggested_answers.length > 0) {\n qHtml += '<div class=\"options-group\" data-q=\"' + i + '\">';\n q.suggested_answers.forEach((sa) => {\n const saEsc = String(sa).replace(/\"/g, '"');\n qHtml += '<label class=\"option-label\"><input type=\"radio\" name=\"q' + i + '\" value=\"' + saEsc + '\"> ' + sa + '</label>';\n });\n qHtml += '<label class=\"option-label\"><input type=\"radio\" name=\"q' + i + '\" value=\"__custom__\"> Altro (testo libero)</label>';\n qHtml += '</div>';\n qHtml += '<textarea class=\"custom-area\" name=\"q' + i + '_custom\" placeholder=\"Scrivi la tua risposta personalizzata...\"></textarea>';\n } else {\n qHtml += '<textarea class=\"freetext\" name=\"q' + i + '\" placeholder=\"Scrivi la tua risposta...\" required></textarea>';\n }\n qHtml += '</div>';\n});\nif (questions.length === 0) {\n qHtml += '<p style=\"text-align:center;color:#6b7280;padding:20px\">Nessuna domanda generata. Il modello potrebbe necessitare di una revisione manuale.</p>';\n}\n\nreturn [{\n json: {\n session_id: prepData.session_id,\n iteration: prepData.iteration,\n max_iterations: maxIter,\n domain_model: prepData.domain_model,\n domain_model_b64: domainModelB64,\n file_name: prepData.file_name || '',\n status: apiResult.status,\n follow_up_questions: JSON.stringify(questions),\n refined_model: JSON.stringify(refinedModel),\n summary: JSON.stringify(apiResult.summary || {}),\n semantic_issues: JSON.stringify(apiResult.semantic_issues || []),\n conflict_issues: JSON.stringify(apiResult.conflict_issues || []),\n questions_html: qHtml.replace(/{{/g, '{{').replace(/}}/g, '}}')\n }\n}];"
},
"id": "merge-context",
"name": "Combina Risultati",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1800,
620
],
"notes": "Unisce i risultati dell'analisi API con il contesto della sessione"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "check-valid",
"leftValue": "={{ $json.status }}",
"rightValue": "VALID",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "check-valid",
"name": "Modello Valido?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2020,
620
],
"notes": "VALID -> Successo | ISSUES_FOUND -> Form domande"
},
{
"parameters": {
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html lang=\"it\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Modello DDD Validato!</title>\n <style>\n *{margin:0;padding:0;box-sizing:border-box}\n body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:linear-gradient(135deg,#10b981 0%,#059669 100%);min-height:100vh;display:flex;justify-content:center;align-items:flex-start;padding:40px 20px}\n .container{background:#fff;max-width:900px;width:100%;border-radius:20px;padding:60px 40px;box-shadow:0 30px 90px rgba(0,0,0,0.4);animation:fadeIn .6s}\n @keyframes fadeIn{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}\n .top-section{text-align:center}\n .icon{font-size:100px;margin-bottom:24px}\n h1{color:#059669;font-size:36px;margin-bottom:16px}\n .message{color:#6b7280;line-height:1.8;margin-bottom:28px;font-size:16px}\n .stats{background:linear-gradient(135deg,#f0fdf4 0%,#d1fae5 100%);border-radius:15px;padding:30px;margin:30px 0;text-align:left;border:2px solid #a7f3d0}\n .stat-row{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-bottom:1px solid #a7f3d0}\n .stat-row:last-child{border-bottom:none}\n .stat-label{color:#065f46;font-weight:600;font-size:15px}\n .stat-value{color:#059669;font-weight:700;font-size:17px}\n .output-section{margin-top:32px;text-align:left}\n .output-section h2{color:#065f46;font-size:22px;margin-bottom:16px;display:flex;align-items:center;gap:10px}\n .model-actions{display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap}\n .btn{display:inline-block;padding:14px 32px;border-radius:10px;text-decoration:none;font-weight:600;font-size:14px;transition:all .3s;margin:0;border:none;cursor:pointer}\n .btn-primary{background:linear-gradient(135deg,#10b981,#059669);color:#fff}\n .btn-download{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff}\n .btn-copy{background:linear-gradient(135deg,#8b5cf6,#7c3aed);color:#fff}\n .btn-toggle{background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff}\n .btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,0.2)}\n .btn-sm{padding:10px 20px;font-size:13px}\n .model-preview{background:#0f172a;border-radius:12px;padding:24px;max-height:500px;overflow:auto;display:none;margin-bottom:20px;position:relative}\n .model-preview.visible{display:block}\n .model-preview pre{color:#e2e8f0;font-family:'Cascadia Code','Fira Code',Consolas,monospace;font-size:13px;line-height:1.6;margin:0;white-space:pre-wrap;word-break:break-word}\n .model-preview .json-key{color:#7dd3fc}\n .model-preview .json-str{color:#86efac}\n .model-preview .json-num{color:#fbbf24}\n .model-preview .json-bool{color:#f472b6}\n .model-preview .json-null{color:#94a3b8}\n .copy-toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%);background:#065f46;color:#fff;padding:12px 28px;border-radius:10px;font-weight:600;font-size:14px;opacity:0;transition:opacity .3s;pointer-events:none;z-index:100}\n .copy-toast.show{opacity:1}\n .actions-bottom{text-align:center;margin-top:28px;display:flex;justify-content:center;gap:12px;flex-wrap:wrap}\n .footer{text-align:center;margin-top:28px;color:#9ca3af;font-size:12px}\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"top-section\">\n <div class=\"icon\">✅</div>\n <h1>Modello DDD Validato!</h1>\n <p class=\"message\">\n <strong>Complimenti!</strong> Dopo <strong>{{ $json.iteration + 1 }}</strong> {{ $json.iteration === 0 ? 'iterazione' : 'iterazioni' }} di analisi,<br>\n il modello Domain-Driven Design e' ora <strong>completo e coerente</strong>.\n </p>\n </div>\n <div class=\"stats\">\n <div class=\"stat-row\">\n <span class=\"stat-label\">Iterazioni completate</span>\n <span class=\"stat-value\">{{ $json.iteration + 1 }}</span>\n </div>\n <div class=\"stat-row\">\n <span class=\"stat-label\">Status finale</span>\n <span class=\"stat-value\">VALID</span>\n </div>\n <div class=\"stat-row\">\n <span class=\"stat-label\">Session ID</span>\n <span class=\"stat-value\" style=\"font-size:11px;font-family:monospace\">{{ $json.session_id }}</span>\n </div>\n </div>\n <div class=\"output-section\">\n <h2>Modello Raffinato (Output)</h2>\n <div class=\"model-actions\">\n <button class=\"btn btn-download btn-sm\" id=\"downloadBtn\">Scarica JSON</button>\n <button class=\"btn btn-copy btn-sm\" id=\"copyBtn\">Copia negli appunti</button>\n <button class=\"btn btn-toggle btn-sm\" id=\"toggleBtn\">Mostra/Nascondi JSON</button>\n </div>\n <div class=\"model-preview\" id=\"modelPreview\"><pre id=\"modelJson\"></pre></div>\n </div>\n <div class=\"actions-bottom\">\n <a href=\"/\" class=\"btn btn-primary\">Torna a n8n</a>\n </div>\n <div class=\"footer\">Agent 2 - Consistency & Conflict Analyzer | Powered by n8n + Ollama</div>\n </div>\n <div class=\"copy-toast\" id=\"toast\">Copiato negli appunti!</div>\n <script>\n let modelStr = '';\n try {\n modelStr = JSON.stringify(JSON.parse(atob('{{ $json.domain_model_b64 }}')), null, 2);\n document.getElementById('modelJson').textContent = modelStr;\n } catch(e) {\n document.getElementById('modelJson').textContent = 'Errore nel caricamento del modello: ' + e.message;\n }\n document.getElementById('toggleBtn').addEventListener('click', function() {\n document.getElementById('modelPreview').classList.toggle('visible');\n });\n document.getElementById('downloadBtn').addEventListener('click', function() {\n const blob = new Blob([modelStr], {type:'application/json'});\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'refined_model.json';\n a.click();\n URL.revokeObjectURL(url);\n });\n document.getElementById('copyBtn').addEventListener('click', function() {\n navigator.clipboard.writeText(modelStr).then(function() {\n const t = document.getElementById('toast');\n t.classList.add('show');\n setTimeout(function(){ t.classList.remove('show'); }, 2000);\n });\n });\n </script>\n</body>\n</html>",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
}
]
}
}
},
"id": "success-page",
"name": "Pagina Successo",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
2260,
500
],
"notes": "Pagina finale di successo - modello validato"
},
{
"parameters": {
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html lang=\"it\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Agent 2 - Domande di Validazione</title>\n <style>\n *{margin:0;padding:0;box-sizing:border-box}\n body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);min-height:100vh;padding:20px;display:flex;justify-content:center;align-items:flex-start;padding-top:40px}\n .container{background:#fff;max-width:900px;width:100%;border-radius:20px;box-shadow:0 30px 90px rgba(0,0,0,0.4);overflow:hidden;animation:slideIn .5s}\n @keyframes slideIn{from{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}\n .header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:36px 40px;text-align:center}\n .header h1{font-size:28px;margin-bottom:8px}\n .header p{opacity:.9;font-size:14px}\n .content{padding:40px}\n .iter-badge{display:inline-block;background:linear-gradient(135deg,#fbbf24,#f59e0b);color:#78350f;padding:6px 18px;border-radius:20px;font-size:13px;font-weight:700;margin-bottom:22px}\n .summary{background:linear-gradient(135deg,#fef3c7,#fde68a);border-left:5px solid #f59e0b;padding:18px;margin-bottom:30px;border-radius:8px}\n .summary h3{color:#78350f;margin-bottom:12px;font-size:16px}\n .summary-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}\n .summary-item{background:#fff;padding:8px 14px;border-radius:6px;font-size:13px}\n .summary-item strong{color:#92400e}\n .q-block{background:linear-gradient(135deg,#f9fafb,#f3f4f6);border:2px solid #e5e7eb;border-radius:12px;padding:28px;margin-bottom:26px;transition:all .3s;position:relative}\n .q-block:hover{border-color:#667eea;box-shadow:0 6px 20px rgba(102,126,234,0.12)}\n .q-num{position:absolute;top:-14px;left:18px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:15px;box-shadow:0 4px 12px rgba(102,126,234,0.3)}\n .q-label{font-weight:600;color:#1f2937;margin-bottom:14px;margin-top:8px;font-size:16px;line-height:1.5}\n .severity{display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;margin-left:8px;font-weight:700;vertical-align:middle}\n .sev-CRITICAL{background:#fee2e2;color:#991b1b}\n .sev-HIGH{background:#fed7aa;color:#9a3412}\n .sev-MEDIUM{background:#fef3c7;color:#78350f}\n .sev-LOW{background:#dbeafe;color:#1e40af}\n .context-info{font-size:12px;color:#6b7280;margin-bottom:12px;padding:8px 12px;background:#fff;border-radius:6px;border:1px solid #e5e7eb}\n .options-group{margin-top:10px}\n .option-label{display:block;padding:10px 14px;margin-bottom:6px;border:2px solid #e5e7eb;border-radius:10px;cursor:pointer;transition:all .2s;font-size:14px;line-height:1.4}\n .option-label:hover{border-color:#667eea;background:#f5f3ff}\n .option-label input[type=radio]{margin-right:10px;accent-color:#667eea}\n .option-label.selected{border-color:#667eea;background:#ede9fe}\n .custom-area{width:100%;padding:14px;border:2px solid #d1d5db;border-radius:10px;font-size:14px;font-family:inherit;resize:vertical;min-height:80px;transition:all .3s;margin-top:8px;display:none}\n .custom-area:focus{outline:none;border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,0.1)}\n .custom-area.visible{display:block}\n textarea.freetext{width:100%;padding:14px;border:2px solid #d1d5db;border-radius:10px;font-size:14px;font-family:inherit;resize:vertical;min-height:100px;transition:all .3s}\n textarea.freetext:focus{outline:none;border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,0.1)}\n .submit-btn{width:100%;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;padding:18px;border-radius:12px;font-size:16px;font-weight:700;cursor:pointer;transition:all .3s;margin-top:24px;box-shadow:0 8px 20px rgba(102,126,234,0.3)}\n .submit-btn:hover{transform:translateY(-2px);box-shadow:0 12px 30px rgba(102,126,234,0.4)}\n .submit-btn:disabled{opacity:.6;cursor:not-allowed;transform:none}\n .footer{text-align:center;padding:20px;color:#9ca3af;font-size:12px;border-top:1px solid #e5e7eb;background:#f9fafb}\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1>Agent 2 - Validazione DDD</h1>\n <p>Rispondi alle domande per raffinare il modello collaborativamente</p>\n </div>\n <div class=\"content\">\n <div class=\"iter-badge\">Iterazione {{ $json.iteration + 1 }} di {{ $json.max_iterations }}</div>\n <div class=\"summary\">\n <h3>Riepilogo Problemi</h3>\n <div class=\"summary-grid\">\n <div class=\"summary-item\"><strong>{{ JSON.parse($json.summary).total_issues || 0 }}</strong> Problemi totali</div>\n <div class=\"summary-item\"><strong>{{ JSON.parse($json.summary).entity_overlaps || 0 }}</strong> Entity overlaps</div>\n <div class=\"summary-item\"><strong>{{ JSON.parse($json.summary).requirement_conflicts || 0 }}</strong> Conflitti requisiti</div>\n <div class=\"summary-item\"><strong>{{ JSON.parse($json.summary).auto_fixed || 0 }}</strong> Auto-corretti</div>\n </div>\n </div>\n <form id=\"answersForm\">\n {{ $json.questions_html }}\n <button type=\"submit\" class=\"submit-btn\" id=\"submitBtn\">Invia Risposte e Continua Raffinamento</button>\n </form>\n </div>\n <div class=\"footer\">Agent 2 - Consistency & Conflict Analyzer | Powered by n8n + Ollama (Qwen2.5 14B)</div>\n </div>\n <script>\n // Radio button selection highlighting and custom textarea toggle\n document.querySelectorAll('.option-label input[type=radio]').forEach(radio => {\n radio.addEventListener('change', function() {\n const group = this.closest('.options-group');\n group.querySelectorAll('.option-label').forEach(l => l.classList.remove('selected'));\n this.closest('.option-label').classList.add('selected');\n const customArea = group.nextElementSibling;\n if (customArea && customArea.classList.contains('custom-area')) {\n if (this.value === '__custom__') {\n customArea.classList.add('visible');\n customArea.focus();\n } else {\n customArea.classList.remove('visible');\n }\n }\n });\n });\n\n // Form submission\n document.getElementById('answersForm').addEventListener('submit', async function(e) {\n e.preventDefault();\n const form = this;\n const btn = document.getElementById('submitBtn');\n const questionCount = parseInt(document.getElementById('question_count').value);\n\n // Collect answers\n const answers = {};\n for (let i = 0; i < questionCount; i++) {\n const radio = form.querySelector('input[name=\"q' + i + '\"]:checked');\n if (radio) {\n if (radio.value === '__custom__') {\n const customVal = form.querySelector('textarea[name=\"q' + i + '_custom\"]').value.trim();\n if (!customVal) { alert('Inserisci la risposta personalizzata per la domanda ' + (i+1)); return; }\n answers['q' + i] = customVal;\n } else {\n answers['q' + i] = radio.value;\n }\n } else {\n const textarea = form.querySelector('textarea[name=\"q' + i + '\"]');\n if (textarea) {\n if (!textarea.value.trim()) { alert('Rispondi alla domanda ' + (i+1)); return; }\n answers['q' + i] = textarea.value.trim();\n }\n }\n }\n\n // Disable form\n btn.textContent = 'Elaborazione in corso...';\n btn.disabled = true;\n form.querySelectorAll('input,textarea').forEach(el => el.disabled = true);\n\n const data = {\n session_id: form.querySelector('[name=session_id]').value,\n iteration: parseInt(form.querySelector('[name=iteration]').value),\n max_iterations: parseInt(form.querySelector('[name=max_iterations]').value),\n domain_model_b64: form.querySelector('[name=domain_model_b64]').value,\n file_name: (form.querySelector('[name=file_name]') || {}).value || '',\n answers: answers\n };\n\n try {\n const res = await fetch(window.location.href.replace('agent2-start', 'agent2-submit'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(data)\n });\n if (res.ok) {\n const html = await res.text();\n if (!html || html.trim() === '') { throw new Error('Risposta vuota dal server. Controlla i log di n8n.'); }\n document.open();\n document.write(html);\n document.close();\n } else {\n throw new Error('HTTP ' + res.status);\n }\n } catch(err) {\n alert('Errore: ' + err.message);\n btn.textContent = 'Invia Risposte e Continua Raffinamento';\n btn.disabled = false;\n form.querySelectorAll('input,textarea').forEach(el => el.disabled = false);\n }\n });\n </script>\n</body>\n</html>",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
}
]
}
}
},
"id": "questions-form",
"name": "Form Domande",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
2260,
740
],
"notes": "Form HTML con radio buttons per suggested_answers + textarea per risposte libere.\nDomain model codificato in base64 per sicurezza."
},
{
"parameters": {
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html lang=\"it\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>In Attesa di Agent 1</title>\n <style>\n *{margin:0;padding:0;box-sizing:border-box}\n body{font-family:'Segoe UI',Tahoma,sans-serif;background:radial-gradient(circle at top,#0f172a,#020617);min-height:100vh;display:flex;justify-content:center;align-items:center;padding:20px;color:#fff}\n .panel{background:rgba(15,23,42,0.85);border:1px solid rgba(148,163,184,0.2);border-radius:18px;padding:48px 40px;max-width:640px;text-align:center;box-shadow:0 24px 70px rgba(2,6,23,0.6)}\n h1{font-size:26px;margin-bottom:14px}\n p{color:#cbd5e1;line-height:1.7;font-size:15px}\n .pulse-container{margin:28px auto 0;display:flex;justify-content:center;gap:12px}\n .pulse-dot{width:14px;height:14px;border-radius:50%;background:#38bdf8;animation:pulse 1.4s infinite}\n .pulse-dot:nth-child(2){animation-delay:.2s}\n .pulse-dot:nth-child(3){animation-delay:.4s}\n @keyframes pulse{0%,100%{transform:scale(.8);opacity:.5}50%{transform:scale(1.2);opacity:1}}\n .info{margin-top:28px;padding:16px;background:rgba(56,189,248,0.1);border:1px solid rgba(56,189,248,0.2);border-radius:10px;font-size:13px;color:#7dd3fc}\n code{background:rgba(255,255,255,0.1);padding:2px 8px;border-radius:4px;font-size:12px}\n </style>\n</head>\n<body>\n <div class=\"panel\">\n <h1>In attesa del modello da Agent 1</h1>\n <p>Agent 2 e' pronto e in ascolto.<br>Quando Agent 1 invia il modello via A2A Protocol, l'analisi partira' automaticamente.</p>\n <div class=\"pulse-container\">\n <div class=\"pulse-dot\"></div>\n <div class=\"pulse-dot\"></div>\n <div class=\"pulse-dot\"></div>\n </div>\n <div class=\"info\">\n Endpoint A2A: <code>POST /a2a/message/send</code><br>\n Webhook: <code>POST /webhook/agent2-start</code> con <code>{\"domain_model\": {...}}</code>\n </div>\n </div>\n</body>\n</html>",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
}
]
}
}
},
"id": "a2a-wait",
"name": "Attesa A2A",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
920,
660
],
"notes": "Pagina di attesa per il modello da Agent 1 via A2A Protocol"
}
],
"connections": {
"START": {
"main": [
[
{
"node": "Pagina Scelta",
"type": "main",
"index": 0
}
]
]
},
"SUBMIT": {
"main": [
[
{
"node": "Detect Request Type",
"type": "main",
"index": 0
}
]
]
},
"Detect Request Type": {
"main": [
[
{
"node": "Route",
"type": "main",
"index": 0
}
]
]
},
"Route": {
"main": [
[
{
"node": "Pagina Scelta",
"type": "main",
"index": 0
}
],
[
{
"node": "Lista File API",
"type": "main",
"index": 0
}
],
[
{
"node": "Carica File",
"type": "main",
"index": 0
}
],
[
{
"node": "Attesa A2A",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepara Dati",
"type": "main",
"index": 0
}
]
]
},
"Lista File API": {
"main": [
[
{
"node": "Seleziona File",
"type": "main",
"index": 0
}
]
]
},
"Carica File": {
"main": [
[
{
"node": "Prepara Dati",
"type": "main",
"index": 0
}
]
]
},
"Prepara Dati": {
"main": [
[
{
"node": "Analizza Modello",
"type": "main",
"index": 0
}
]
]
},
"Analizza Modello": {
"main": [
[
{
"node": "Combina Risultati",
"type": "main",
"index": 0
}
]
]
},
"Combina Risultati": {
"main": [
[
{
"node": "Modello Valido?",
"type": "main",
"index": 0
}
]
]
},
"Modello Valido?": {
"main": [
[
{
"node": "Pagina Successo",
"type": "main",
"index": 0
}
],
[
{
"node": "Form Domande",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 2,
"updatedAt": "2026-02-12T12:00:00.000Z",
"versionId": "2"
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Agent 2 - Loop Completo con Form HTML. Uses httpRequest. Webhook trigger; 15 nodes.
Source: https://github.com/claudio-dragotta/agent-consistency-analyzer/blob/9af2297cdd834d4d3c5b0a21e96ce97a08915060/n8n/workflow_complete_loop.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.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c
Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.
📡 This workflow serves as the central Alpha Vantage API fetcher for Tesla trading indicators, delivering cleaned 20-point JSON outputs for three timeframes: , , and . It is required by the following a