AutomationFlowsWeb Scraping › HTML Form File Upload to API

HTML Form File Upload to API

Original n8n title: Agent 2 - Loop Completo Con Form HTML

Agent 2 - Loop Completo con Form HTML. Uses httpRequest. Webhook trigger; 15 nodes.

Webhook trigger★★★★☆ complexity15 nodesHTTP Request
Web Scraping Trigger: Webhook Nodes: 15 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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\">&#128194;</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\">&#128279;</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 &amp; 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\">&#8592; 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\">&#128269;</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&ograve; 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, '&quot;');\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, '&#123;&#123;').replace(/}}/g, '&#125;&#125;')\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\">&#9989;</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 &amp; 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 &amp; 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"
}
Pro

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 →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

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

n8n, Execute Workflow Trigger, HTTP Request +1
Web Scraping

This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .

HTTP Request, Ssh
Web Scraping

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

HTTP Request
Web Scraping

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.

Execute Command, HTTP Request, Read Write File +1
Web Scraping

📡 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

HTTP Request