Infinite loading on Form Node

Describe the problem/error/question

I am building a multi-step form workflow that receives a PDF document via a Form Trigger, extracts the text using an OCR API (returning Markdown), and then displays the parsed results for human validation using an in-workflow Form node (Action). After the user’s approval, an email containing the formatted data should be sent.

Unfortunately, after adding the in-workflow Form node, the execution enters an infinite loading state upon submitting the initial trigger form. The workflow never stops loading, the front-end never redirects to the second form step, and the canvas nodes continuously show “loading data”, preventing me from inspecting the outputs or proceeding with the workflow.

What is the error message (if any)?

There is no explicit crash or error log. The UI simply hangs indefinitely with the nodes showing “loading data”, and the workflow execution never reaches a resolved state.

Please share your workflow

Here is my workflow:

{
“nodes”: [
{
“parameters”: {
“formTitle”: “Portal de Contratos - Conteco”,
“formDescription”: “Faça o upload do PDF do contrato de experiência para agendar os avisos automáticos.”,
“formFields”: {
“values”: [
{
“fieldLabel”: “Arquivo PDF”,
“fieldType”: “file”,
“requiredField”: true
},
{
“fieldLabel”: “Informe o seu email”,
“fieldType”: “email”,
“requiredField”: true
},
{
“fieldLabel”: “Para qual email deve ser encaminhado o aviso?”,
“fieldType”: “email”,
“requiredField”: true
}
]
},
“options”: {
“respondWithOptions”: {
“values”: {
“formSubmittedText”: “Seu PDF fo submetido.”
}
}
}
},
“type”: “n8n-nodes-base.formTrigger”,
“typeVersion”: 2.6,
“position”: [
-80,
-16
],
“id”: “d865f634-c349-4f0e-9293-c307bb536310”,
“name”: “Formaulário de envio de Contratos”,
“webhookId”: “REMOVIDO_POR_SEGURANCA”
},
{
“parameters”: {
“method”: “POST”,
“url”: “https://api.mistral.ai/v1/files”,
“authentication”: “predefinedCredentialType”,
“nodeCredentialType”: “mistralCloudApi”,
“sendHeaders”: true,
“headerParameters”: {
“parameters”: [
{
“name”: “Authorization”,
“value”: “Bearer $MISTRAL_API_KEY”
}
]
},
“sendBody”: true,
“contentType”: “multipart-form-data”,
“bodyParameters”: {
“parameters”: [
{
“name”: “purpose”,
“value”: “ocr”
},
{
“parameterType”: “formBinaryData”,
“name”: “file”,
“inputDataFieldName”: “Arquivo_PDF”
}
]
},
“options”: {}
},
“type”: “n8n-nodes-base.httpRequest”,
“typeVersion”: 4.4,
“position”: [
384,
-16
],
“id”: “f86ce096-e582-430a-9578-09a4a13881d8”,
“name”: “Envia PDF p/ Mistral”,
“credentials”: {
“mistralCloudApi”: {
“id”: “REMOVIDO_POR_SEGURANCA”,
“name”: “Mistral Cloud account”
}
}
},
{
“parameters”: {
“url”: “=https://api.mistral.ai/v1/files/{{ $json.id }}/url”,
“authentication”: “predefinedCredentialType”,
“nodeCredentialType”: “mistralCloudApi”,
“sendQuery”: true,
“queryParameters”: {
“parameters”: [
{
“name”: “expiry”,
“value”: “24”
}
]
},
“sendHeaders”: true,
“headerParameters”: {
“parameters”: [
{
“name”: “Accept”,
“value”: “application/json”
},
{
“name”: “Authorization”,
“value”: “Bearer $MISTRAL_API_KEY”
}
]
},
“options”: {}
},
“type”: “n8n-nodes-base.httpRequest”,
“typeVersion”: 4.4,
“position”: [
592,
-16
],
“id”: “dfa197de-3eb2-4238-acdd-0c6d4983a6c6”,
“name”: “OCR Mistral”,
“credentials”: {
“mistralCloudApi”: {
“id”: “REMOVIDO_POR_SEGURANCA”,
“name”: “Mistral Cloud account”
}
}
},
{
“parameters”: {
“method”: “POST”,
“url”: “https://api.mistral.ai/v1/ocr”,
“authentication”: “predefinedCredentialType”,
“nodeCredentialType”: “mistralCloudApi”,
“sendHeaders”: true,
“headerParameters”: {
“parameters”: [
{
“name”: “Authorization”,
“value”: “Bearer ${MISTRAL_API_KEY}”
}
]
},
“sendBody”: true,
“specifyBody”: “json”,
“jsonBody”: "={\n “model”: “mistral-ocr-latest”,\n “document”: {\n “type”: “document_url”,\n “document_url”: “{{ $json.url }}”\n },\n “table_format”: “html”,\n “include_image_base64”: true\n} ",
“options”: {
“response”: {
“response”: {
“responseFormat”: “file”,
“outputPropertyName”: “ocr_output.json”
}
}
}
},
“type”: “n8n-nodes-base.httpRequest”,
“typeVersion”: 4.4,
“position”: [
800,
-16
],
“id”: “d0f90d0d-bc89-4464-8098-aad75b4ff0af”,
“name”: “Recebe OCR”,
“credentials”: {
“mistralCloudApi”: {
“id”: “REMOVIDO_POR_SEGURANCA”,
“name”: “Mistral Cloud account”
}
}
},
{
“parameters”: {
“operation”: “fromJson”,
“binaryPropertyName”: “ocr_output.json”,
“options”: {
“encoding”: “utf8”
}
},
“type”: “n8n-nodes-base.extractFromFile”,
“typeVersion”: 1.1,
“position”: [
976,
-16
],
“id”: “18869f1d-fde6-488d-923d-1758fe863886”,
“name”: “Extract from File1”
},
{
“parameters”: {
“jsCode”: “const data = $input.first().json.data.pages;\n\nreturn data.map(entry => {\n // 1. Criamos uma variável mutável para o texto do markdown\n let textoCompleto = entry.markdown;\n\n // 2. Verificamos se existem tabelas nesta página\n if (entry.tables && entry.tables.length > 0) {\n \n // 3. Percorremos cada tabela encontrada\n entry.tables.forEach(table => {\n // Montamos a string exata que o OCR deixou como referência\n const referencia = [${table.id}](${table.id});\n \n // 4. Substituímos a referência pelo conteúdo HTML da tabela\n // Usamos split e join para garantir que substitua exatamente o texto, sem problemas com caracteres especiais\n textoCompleto = textoCompleto.split(referencia).join(table.content);\n });\n }\n\n // Retornamos o objeto com o markdown já unificado com as tabelas\n return {\n json: {\n markdown: textoCompleto\n }\n };\n});”
},
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [
1152,
-16
],
“id”: “9f106006-6e4b-4df1-99b5-bf2ff873188e”,
“name”: “Code in JavaScript”
},
{
“parameters”: {
“assignments”: {
“assignments”: [
{
“id”: “cd38ab23-9da2-427c-bd7a-84fdadffe163”,
“name”: “text”,
“value”: “={{ $json.markdown }}”,
“type”: “string”
}
]
},
“options”: {}
},
“type”: “n8n-nodes-base.set”,
“typeVersion”: 3.4,
“position”: [
1408,
-16
],
“id”: “083060b0-068a-425c-858c-3c873cb2ea78”,
“name”: “Edit Fields”
},
{
“parameters”: {
“jsCode”: “const text = $input.first().json.text;\n\n// 1. Extrair o nome da empresa\nconst empresaMatch = text.match(/Empresa:\s*(.)/i);\nlet empresa = empresaMatch ? empresaMatch[1].trim() : “Empresa não identificada”;\n\n// Remove o código numérico e o traço inicial (Ex: "431 - ")\n// ^ → Início da string\n// \d+ → Um ou mais números\n// \s → Zero ou mais espaços\n// - → O caractere de traço\nempresa = empresa.replace(/^\d+\s*-\s*/, ‘’).trim();\n\n// 2. Extrair apenas as linhas da tabela (entre e )\nconst rowRegex = /(.?)</tr>/gs;\nconst cellRegex = /(.?)</td>/g;\nconst rows = […text.matchAll(rowRegex)];\n\nlet tabelaHtml = \n<div style="font-family: sans-serif; border: 1px solid #ddd; padding: 15px; border-radius: 8px;">\n <h3 style="color: #2c3e50;">Empresa: ${empresa}</h3>\n <table style="width: 100%; border-collapse: collapse;">\n <thead>\n <tr style="background-color: #f8f9fa; border-bottom: 2px solid #dee2e6;">\n <th style="text-align: left; padding: 10px; border: 1px solid #dee2e6;">Funcionário</th>\n <th style="text-align: center; padding: 10px; border: 1px solid #dee2e6;">Início</th>\n <th style="text-align: center; padding: 10px; border: 1px solid #dee2e6;">Fim Experiência</th>\n <th style="text-align: center; padding: 10px; border: 1px solid #dee2e6;">Período</th>\n </tr>\n </thead>\n <tbody>;\n\n// 3. Processar cada linha\nrows.forEach((row, index) => {\n const cells = […row[1].matchAll(cellRegex)].map(c => c[1]);\n\n // Validamos se a linha tem pelo menos 8 colunas\n if (cells.length >= 8) {\n const nome = cells[1]; // Coluna: Nome do empregado\n const inicio = cells[4]; // Coluna: Data início contr.\n const fimContrato = cells[5]; // Coluna: Data fim contr.\n const diasContrato = cells[6]; // Coluna: Dias contr.\n const fimProrrog = cells[7]; // Coluna: Data fim prorrog.\n \n // Como a coluna 8 pode estar vazia (sem final), fazemos uma verificação segura\n const diasProrrog = cells[8] ? cells[8].trim() : ""; \n\n // Lógica da Data Final\n const temProrrogacao = (fimProrrog && fimProrrog.trim() !== "" && !fimProrrog.includes(" "));\n const dataFinal = temProrrogacao ? fimProrrog : fimContrato;\n\n // Lógica do Período (Transforma em número inteiro para poder somar)\n const numDiasIniciais = parseInt(diasContrato) || 0;\n const numDiasProrrog = parseInt(diasProrrog) || 0;\n const totalDias = numDiasIniciais + numDiasProrrog;\n\n // Formata o texto final do período (ex: "90 dias (45+45)" ou "45 dias")\n let textoPeriodo = ${totalDias} dias;\n if (numDiasProrrog > 0) {\n textoPeriodo += (${numDiasIniciais}+${numDiasProrrog});\n }\n\n tabelaHtml += \n <tr>\n <td style="padding: 10px; border: 1px solid #dee2e6;\">${nome}</td>\n <td style="padding: 10px; text-align: center; border: 1px solid #dee2e6;\">${inicio}</td>\n <td style="padding: 10px; text-align: center; border: 1px solid #dee2e6; font-weight: bold; color: #d9534f;\">${dataFinal}</td>\n <td style="padding: 10px; text-align: center; border: 1px solid #dee2e6; color: #555;\">${textoPeriodo}</td>\n </tr>;\n }\n});\n\ntabelaHtml += </tbody></table></div>;\n\nreturn {\n html_final: tabelaHtml,\n empresa: empresa\n};”
},
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [
1920,
-192
],
“id”: “5876b2ff-4624-4669-babe-88bb0515606d”,
“name”: “Code in JavaScript1”
},
{
“parameters”: {
“toEmail”: “={{ $(‘Formaulário de envio de Contratos’).item.json[‘Para qual email deve ser encaminhado o aviso?’] }}”,
“subject”: “=Aviso de Fim de Contrato de Experiência - {{ $json.empresa }}”,
“html”: “=<div style="font-family: Arial, sans-serif; color: #333;">\n\n\nOlá, equipe da Conteco,\n\n\n\nUm novo documento de contrato foi processado pelo sistema. Confira os dados extraídos abaixo:\n\n\n \n <div style="margin: 20px 0;">\n {{ $json.html_final}}\n \n\n <p style="font-size: 12px; color: #777;">\n Este é um envio automático do Assistente de IA interno.\n\n\n\n”,
“options”: {
“appendAttribution”: false,
“ccEmail”: “={{ $(‘Formaulário de envio de Contratos’).item.json[‘Informe o seu email’] }}”
}
},
“type”: “n8n-nodes-base.emailSend”,
“typeVersion”: 2.1,
“position”: [
2832,
-192
],
“id”: “3c7ee9b9-395f-4a0f-b861-3e41d220da1c”,
“name”: “Send an Email”,
“webhookId”: “REMOVIDO_POR_SEGURANCA”,
“credentials”: {
“smtp”: {
“id”: “REMOVIDO_POR_SEGURANCA”,
“name”: “SMTP account”
}
}
},
{
“parameters”: {
“formFields”: {
“values”: [
{
“fieldType”: “html”,
“html”: “<div style="font-family: Arial, sans-serif; color: #333;">\n\n\nOlá, equipe da Conteco,\n\n\n\nUm novo documento de contrato foi processado pelo sistema. Confira os dados extraídos abaixo:\n\n\n \n <div style="margin: 20px 0;">\n {{ $json.html_final }}\n \n\n <p style="font-size: 12px; color: #777;">\n Este é um envio automático do Assistente de IA interno.\n\n\n\n”
},
{
“fieldLabel”: “Após revisar os dados, você confirma o envio desse email?”,
“fieldType”: “checkbox”,
“fieldOptions”: {
“values”: [
{
“option”: “Sim, confirmo o envio do email.”
},
{
“option”: “Não, o email contém divergências.”
}
]
}
}
]
},
“options”: {}
},
“type”: “n8n-nodes-base.form”,
“typeVersion”: 2.5,
“position”: [
2144,
-192
],
“id”: “06ba2fe9-bf23-48b7-a89f-99905eb19246”,
“name”: “Pré-visualização do E-mail”,
“webhookId”: “REMOVIDO_POR_SEGURANCA”,
“alwaysOutputData”: true
},
{
“parameters”: {
“conditions”: {
“options”: {
“caseSensitive”: true,
“leftValue”: “”,
“typeValidation”: “strict”,
“version”: 3
},
“conditions”: [
{
“id”: “9bf5112e-4c63-416c-94f9-d73524758fa3”,
“leftValue”: “={{ $json[‘Após revisar os dados, você confirma o envio desse email?’][0] }}”,
“rightValue”: “=Sim, confirmo o envio do email.”,
“operator”: {
“type”: “string”,
“operation”: “equals”
}
}
],
“combinator”: “and”
},
“options”: {}
},
“type”: “n8n-nodes-base.if”,
“typeVersion”: 2.3,
“position”: [
2576,
-192
],
“id”: “50458d2c-6db9-419c-a8f9-a5d930912bf7”,
“name”: “If”
},
{
“parameters”: {
“operation”: “completion”,
“completionTitle”: “Obrigado!”,
“completionMessage”: “O email foi enviado ao devido destinatário.”,
“limitWaitTime”: true,
“resumeAmount”: 2,
“resumeUnit”: “minutes”,
“options”: {}
},
“type”: “n8n-nodes-base.form”,
“typeVersion”: 2.5,
“position”: [
2352,
-192
],
“id”: “9ae6ea32-f7ab-47ac-85d5-6649f061b805”,
“name”: “Form”,
“webhookId”: “REMOVIDO_POR_SEGURANCA”,
“alwaysOutputData”: true
},
{
“parameters”: {},
“type”: “n8n-nodes-base.noOp”,
“typeVersion”: 1,
“position”: [
2832,
-16
],
“id”: “570c6dd6-6b6f-4c7a-b3a5-49e2d46d2d63”,
“name”: “No Operation, do nothing”
}
],
“connections”: {
“Formaulário de envio de Contratos”: {
“main”: [
[
{
“node”: “Envia PDF p/ Mistral”,
“type”: “main”,
“index”: 0
}
]
]
},
“Envia PDF p/ Mistral”: {
“main”: [
[
{
“node”: “OCR Mistral”,
“type”: “main”,
“index”: 0
}
]
]
},
“OCR Mistral”: {
“main”: [
[
{
“node”: “Recebe OCR”,
“type”: “main”,
“index”: 0
}
]
]
},
“Recebe OCR”: {
“main”: [
[
{
“node”: “Extract from File1”,
“type”: “main”,
“index”: 0
}
]
]
},
“Extract from File1”: {
“main”: [
[
{
“node”: “Code in JavaScript”,
“type”: “main”,
“index”: 0
}
]
]
},
“Code in JavaScript”: {
“main”: [
[
{
“node”: “Edit Fields”,
“type”: “main”,
“index”: 0
}
]
]
},
“Edit Fields”: {
“main”: [
[
{
“node”: “Code in JavaScript1”,
“type”: “main”,
“index”: 0
}
]
]
},
“Code in JavaScript1”: {
“main”: [
[
{
“node”: “Pré-visualização do E-mail”,
“type”: “main”,
“index”: 0
}
]
]
},
“Send an Email”: {
“main”: [

]
},
“Pré-visualização do E-mail”: {
“main”: [
[
{
“node”: “Form”,
“type”: “main”,
“index”: 0
}
]
]
},
“If”: {
“main”: [
[
{
“node”: “Send an Email”,
“type”: “main”,
“index”: 0
}
],
[
{
“node”: “No Operation, do nothing”,
“type”: “main”,
“index”: 0
}
]
]
},
“Form”: {
“main”: [
[
{
“node”: “If”,
“type”: “main”,
“index”: 0
}
]
]
}
},
“pinData”: {},
“meta”: {
“templateCredsSetupCompleted”: true,
“instanceId”: “REMOVIDO_POR_SEGURANCA”
}
}

Share the output returned by the last node

Information on your n8n setup

  • Debug info

    core

    • n8nVersion: 2.32.5
    • platform: docker (self-hosted)
    • nodeJsVersion: 24.18.0
    • nodeEnv: production
    • database: postgres
    • executionMode: regular
    • concurrency: -1
    • license: enterprise (production)

    storage

    • success: none
    • error: none
    • progress: false
    • manual: false
    • binaryMode: filesystem

    pruning

    • enabled: true
    • maxAge: 168 hours
    • maxCount: 50000 executions

    client

    • userAgent: mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/150.0.0.0 safari/537.36
    • isTouchDevice: false

Hey @guilherme_aquino, while you wait for a response, here are some things that might help:

Suggested resources

Automatically matched to your question.

Docs:

Forum:

@jmoura, @Jean_Saucisse, @krisn0x - you’ve helped with similar issues before, can you take a look?

Automatically suggested by n8n’s community bot. It’s a pilot - please share feedback here.

Hi @guilherme_aquino Welcome!
n8n serves form pages with a CSP sandbox header that omits allow-same-origin, so the browser treats the page as origin null and blocks its poll to /form-waiting/<executionId>/n8n-execution-status. The execution stays in waiting, the second page never renders, and nothing errors, which is the state you’re in. Open the browser console on the stuck page and the blocked request will be there. Set this and restart n8n:

N8N_INSECURE_DISABLE_FORM_HTML_SANDBOX=true

That variable landed in 2.15.0, so it is available on your 2.32.5. It removes the sandbox from every form page, so serve your forms from a separate subdomain if other people share this instance.
See this:

Hi, thanks so much for the reply! Sorry if my initial post wasn’t clear.

The issue isn’t on the front-end. The user-facing forms are rendering perfectly and the CORS/CSP is fine (I actually added the environment variable you suggested just to be absolutely sure), and the workflow finishes the execution without throwing any errors.

The problem is strictly happening in the n8n UI / Canvas. The nodes get completely stuck on ‘loading data’ for me (the developer), preventing me from inspecting the outputs and building the rest of the workflow.

Additionally, I noticed a secondary issue: the final email is now missing the HTML data generated by the OCR. Before adding this in-workflow Form node, it worked perfectly. My guess is that the data from the previous nodes is dropping or out of scope when passing through the Form node step.

Thanks again for taking the time to help! I’m still learning my way around n8n, so any guidance is hugely appreciated.

The canvas hang is your execution saving config. Your debug info shows manual: false and progress: false, so nothing about this run gets written to the database. The n8n Form node offloads the execution data to the database when it pauses and reloads it when the form comes back, so with saving off there is no record for the editor to read and the nodes sit on “loading data” while the run itself finishes normally. Set these and restart n8n:

EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true
EXECUTIONS_DATA_SAVE_ON_PROGRESS=true

The same two toggles also exist per workflow under Workflow Settings and override the instance values, so check there as well.

On the email, Send an Email reads {{ $json.html_final }}, and downstream of the Form node $json is the form submission, so that field is not there anymore. Reference the Code node directly, and use .first() rather than .item, since paired item linking does not carry across the resume:

{{ $('Code in JavaScript1').first().json.html_final }}

Same pattern for the subject and for the two addresses you pull from the trigger.