Formstep

Read a PDF with AI in n8n, then have the sender confirm the data

Import this workflow and run it as it is. It downloads a sample invoice, reads it with an AI model and sends a Formstep form with every value filled in. The workflow then waits for the sender to confirm, and lists any values they corrected.

Published · Updated

An AI model reads an invoice in seconds and sounds just as sure of every value, including a total with two digits swapped. In the test run for this workflow, it read 27 325 as 27 235.

So the workflow asks the one person who knows the invoice: the sender. Formstep sends them a request, a form on a private link with what the AI read already filled in, and they confirm or fix each value.

In short: An AI model reads the PDF, a Formstep form asks the sender to confirm what it read, and the workflow carries on with checked values and a list of what they changed. Get the workflow JSON.

What you need

Other documents

For orders, claims or delivery notes, build a form for that document and name each attribute of the AI step after one of its field keys. n8n PDF extraction with AI, confirmed by the sender explains each node and the full test run.

How the n8n workflow works, step by step

The workflow in the n8n editor: Sample invoice, Download the PDF, Extract the text, Read the PDF with AI with a Claude model, Keep the file, Ask the sender to confirm, Wait for the sender, and Answered? branching to What the sender corrected or No answer: chase the sender
The imported workflow in n8n 2.46. The purple border marks the pinned sample result, so it runs without an AI key. Select it to see it full size.

1. Download the PDF

A sample invoice downloads over HTTP. For real invoices, swap in a Gmail, Outlook or Google Drive trigger.

2. Extract the text

Extract from File pulls the text out of the PDF. A scanned PDF has no text, so see the questions below.

3. Read the PDF with AI

An Information Extractor with a Claude model returns one value per form question: supplier, invoice number, dates and total.

4. Ask the sender to confirm

The Formstep node sends the Confirm Invoice Details form to the sender, with every value filled in and their PDF shown above it.

5. Wait for the sender

A Wait node pauses the workflow until they submit, or until the request expires after five days.

6. What the sender corrected

A Code node compares the AI’s values with the confirmed ones and lists what changed, so corrected invoices go to a person.

Import the n8n workflow

Paste it onto an n8n canvas with Ctrl+V (Cmd+V on a Mac), or download the file and use Import from File. Then pick your credentials and forms in the nodes marked with a warning.

Download the file
Show the JSON
{
  "name": "Formstep: read a PDF with AI, then have the sender confirm it",
  "nodes": [
    {
      "parameters": {
        "content": "## Read a PDF with AI, then have the sender confirm it\n\nRuns as it is on a sample invoice, with a pinned AI result.\n\n1. **Ask the sender to confirm**: pick your Formstep credential and the published Confirm Invoice Details form.\n2. **Claude**: pick a model credential, or swap in any chat model. Unpin **Read the PDF with AI** once it is connected.\n3. Swap **Sample invoice** and **Download the PDF** for your Gmail, Outlook or Google Drive trigger.\n\nTest Mode is on, so nothing is emailed. Delivery is Email the Invitation, which needs Formstep Pro or Business.",
        "height": 360,
        "width": 640,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -420
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000001",
      "name": "How to set it up"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000002",
      "name": "When clicking ‘Execute workflow’"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7c4d1e2a-2000-4000-8000-000000000003",
              "name": "pdfUrl",
              "value": "https://docs.formstep.io/guides/n8n/samples/invoice.pdf",
              "type": "string"
            },
            {
              "id": "7c4d1e2a-2000-4000-8000-000000000004",
              "name": "senderEmail",
              "value": "billing@fjord-office.example",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000005",
      "name": "Sample invoice"
    },
    {
      "parameters": {
        "url": "={{ $json.pdfUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000006",
      "name": "Download the PDF"
    },
    {
      "parameters": {
        "operation": "pdf",
        "options": {}
      },
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        660,
        160
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000007",
      "name": "Extract the text"
    },
    {
      "parameters": {
        "text": "={{ $json.text }}",
        "attributes": {
          "attributes": [
            {
              "name": "supplier_name",
              "type": "string",
              "description": "The company that issued the invoice",
              "required": true
            },
            {
              "name": "invoice_number",
              "type": "string",
              "description": "The invoice number, as printed",
              "required": true
            },
            {
              "name": "invoice_date",
              "type": "date",
              "description": "The invoice date, as YYYY-MM-DD",
              "required": true
            },
            {
              "name": "due_date",
              "type": "date",
              "description": "The payment due date, as YYYY-MM-DD",
              "required": true
            },
            {
              "name": "total_amount",
              "type": "number",
              "description": "The total to pay, including VAT, as a number without currency",
              "required": true
            }
          ]
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.informationExtractor",
      "typeVersion": 1.2,
      "position": [
        880,
        160
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000008",
      "name": "Read the PDF with AI"
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-5-5",
          "cachedResultName": "Claude Sonnet 5.5"
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.6,
      "position": [
        880,
        380
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000009",
      "name": "Claude"
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        1120,
        0
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000010",
      "name": "Keep the file"
    },
    {
      "parameters": {
        "resource": "request",
        "operation": "create",
        "formId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "recipientEmail": "={{ $('Sample invoice').item.json.senderEmail }}",
        "fields": {
          "mappingMode": "defineBelow",
          "value": {
            "supplier_name": "={{ $json.output.supplier_name }}",
            "invoice_number": "={{ $json.output.invoice_number }}",
            "invoice_date": "={{ $json.output.invoice_date }}",
            "due_date": "={{ $json.output.due_date }}",
            "total_amount": "={{ $json.output.total_amount }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "supplier_name",
              "displayName": "Supplier (supplier_name)",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string"
            },
            {
              "id": "invoice_number",
              "displayName": "Invoice Number (invoice_number)",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string"
            },
            {
              "id": "invoice_date",
              "displayName": "Invoice Date (invoice_date)",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "dateTime"
            },
            {
              "id": "due_date",
              "displayName": "Due Date (due_date)",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "dateTime"
            },
            {
              "id": "total_amount",
              "displayName": "Total Amount (total_amount)",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "number"
            },
            {
              "id": "confirmed",
              "displayName": "I confirm these details are correct (confirmed)",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "boolean",
              "removed": true
            },
            {
              "id": "notes",
              "displayName": "Notes (notes)",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "readonly": [],
        "documents": {
          "values": [
            {
              "binaryProperty": "data",
              "name": "=Invoice {{ $json.output.invoice_number }}",
              "field": ""
            }
          ]
        },
        "waitForOutcome": true,
        "additionalFields": {
          "delivery": "email",
          "externalId": "=invoice-{{ $json.output.invoice_number }}-{{ $execution.id }}",
          "expiresAt": "={{ $now.plus({ days: 5 }).toISO() }}",
          "test": true
        }
      },
      "type": "n8n-nodes-formstep.formstep",
      "typeVersion": 5,
      "position": [
        1340,
        0
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000011",
      "name": "Ask the sender to confirm"
    },
    {
      "parameters": {
        "resume": "webhook",
        "httpMethod": "POST",
        "options": {}
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1560,
        0
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000012",
      "name": "Wait for the sender",
      "webhookId": "7c4d1e2a-2000-4000-8000-000000000013"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "7c4d1e2a-2000-4000-8000-000000000014",
              "leftValue": "={{ $json.body.type }}",
              "rightValue": "request.completed",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1780,
        0
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000015",
      "name": "Answered?"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Compare what the AI read with what the sender confirmed.\nconst read = $('Read the PDF with AI').item.json.output\nconst confirmed = $json.body.data.answers\nconst corrected = Object.keys(read).filter((key) => String(read[key]) !== String(confirmed[key]))\nreturn { json: { invoice: confirmed, corrected, notes: confirmed.notes ?? null } }"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        -100
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000016",
      "name": "What the sender corrected"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7c4d1e2a-2000-4000-8000-000000000017",
              "name": "to",
              "value": "={{ $('Sample invoice').item.json.senderEmail }}",
              "type": "string"
            },
            {
              "id": "7c4d1e2a-2000-4000-8000-000000000018",
              "name": "message",
              "value": "=We still need you to confirm invoice {{ $('Read the PDF with AI').item.json.output.invoice_number }} ({{ $json.body.type }}).",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2000,
        120
      ],
      "id": "7c4d1e2a-2000-4000-8000-000000000019",
      "name": "No answer: chase the sender"
    }
  ],
  "pinData": {
    "Read the PDF with AI": [
      {
        "json": {
          "output": {
            "supplier_name": "Fjord Office Supplies AS",
            "invoice_number": "INV-2026-0187",
            "invoice_date": "2026-10-20",
            "due_date": "2026-11-03",
            "total_amount": 27235
          }
        }
      }
    ]
  },
  "connections": {
    "When clicking ‘Execute workflow’": {
      "main": [
        [
          {
            "node": "Sample invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample invoice": {
      "main": [
        [
          {
            "node": "Download the PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download the PDF": {
      "main": [
        [
          {
            "node": "Extract the text",
            "type": "main",
            "index": 0
          },
          {
            "node": "Keep the file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract the text": {
      "main": [
        [
          {
            "node": "Read the PDF with AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the PDF with AI": {
      "main": [
        [
          {
            "node": "Keep the file",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Claude": {
      "ai_languageModel": [
        [
          {
            "node": "Read the PDF with AI",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Keep the file": {
      "main": [
        [
          {
            "node": "Ask the sender to confirm",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask the sender to confirm": {
      "main": [
        [
          {
            "node": "Wait for the sender",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for the sender": {
      "main": [
        [
          {
            "node": "Answered?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answered?": {
      "main": [
        [
          {
            "node": "What the sender corrected",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No answer: chase the sender",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}

It needs the n8n-nodes-formstep community node on a self-hosted n8n that Formstep can reach over HTTPS: install it and connect it. Checked in n8n 2.46 with version 0.15.0 of the node.

Why use Formstep for this step

  • One run from PDF to checked data. What the AI read and what the sender confirmed meet in one workflow run, with no database in between.
  • The sender checks against the original. The PDF sits at the top of the form, next to every prefilled value.
  • You learn where the AI goes wrong. The corrected list shows which fields the model misreads, invoice after invoice.

More workflow templates

Questions about this workflow

Something else on your mind? The docs cover every setting, and we answer email.

Read the docs

Any chat model the n8n AI nodes take: Anthropic, OpenAI, Google Gemini, Mistral or a local model through Ollama. Swap the model node and keep the attributes.

Not through Extract From PDF, because a scan carries no text. Replace the first two steps with a model node that reads images or PDFs; the rest stays the same.

No. Any published form works if its field keys match the extractor’s attributes. Or set Form Source to New Form and send the form’s blocks with the request.

Add a file upload question keyed corrected_invoice. The file arrives as a download URL under that key, so copy it to your own storage.

The request expires after five days, the Wait node resumes with request.expired, and the false branch of Answered? chases them. On Pro and Business, Formstep sends reminders first.

Collect information from anyone.

Publish a sample form, answer it as your recipient would, and watch the answers come back. No account needed.

Read the docs