{
  "name": "Collect supplier invoices from Gmail into Drive and a Sheets review queue with Repondea",
  "nodes": [
    {
      "id": "c8784de2-dfe2-4168-9228-572ffe8bbb1e",
      "name": "Gmail invoices",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.4,
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "maxResults": 1,
        "filters": {
          "q": "label:Repondea-Factures has:attachment",
          "readStatus": "unread"
        },
        "options": {
          "downloadAttachments": true,
          "dataPropertyAttachmentsPrefixName": "attachment_"
        }
      },
      "position": [
        -1000,
        120
      ]
    },
    {
      "id": "0685f667-f949-4087-8320-23395c8f2012",
      "name": "Split invoice attachments",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "const out = [];\nfor (const [i, item] of $input.all().entries()) {\n  const id = String(item.json.id || '');\n  if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw new Error('Missing Gmail message id');\n  for (const [key, binary] of Object.entries(item.binary || {})) {\n    if (!/^attachment_\\d+$/.test(key)) continue;\n    const name = String(binary.fileName || '');\n    if (!/\\.(pdf|png|jpe?g)$/i.test(name)) continue;\n    const source_key = id + '_' + key;\n    out.push({ json: { source_key, message_id: id, original_filename: name,\n      archive_name: source_key, gmail_url: 'https://mail.google.com/mail/u/0/#all/' + id },\n      binary: { invoice: binary }, pairedItem: { item: i } });\n  }\n}\nif (!out.length) throw new Error('No PDF, PNG or JPEG attachment; message stays unread');\nreturn out;"
      },
      "position": [
        -760,
        120
      ]
    },
    {
      "id": "9ab0fbc7-a455-4f59-8e9c-8158e9040e86",
      "name": "One invoice at a time",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "position": [
        -510,
        120
      ]
    },
    {
      "id": "522ad892-05f3-4e65-abed-002226fc4807",
      "name": "Find existing row",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "YOUR_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Invoices",
          "mode": "name"
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "source_key",
              "lookupValue": "={{ $json.source_key }}"
            }
          ]
        },
        "options": {}
      },
      "position": [
        -280,
        180
      ],
      "alwaysOutputData": true
    },
    {
      "id": "a2b2a97e-580b-414d-96d7-5b6330ff26d7",
      "name": "Already in queue",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "d2a4914d-e708-4acf-9bcf-c1f499334769",
              "leftValue": "={{ String($json.source_key || \"\") }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "position": [
        -50,
        180
      ]
    },
    {
      "id": "6c88a800-9745-4d4d-9d5b-46de69177e96",
      "name": "Restore attachment",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "const original = $('One invoice at a time').item;\nreturn [{ json: original.json, binary: original.binary }];"
      },
      "position": [
        180,
        260
      ]
    },
    {
      "id": "c8c5eff6-3faa-4d57-8441-f8fb95d45275",
      "name": "Find archived original",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "parameters": {
        "resource": "fileFolder",
        "operation": "search",
        "searchMethod": "query",
        "queryString": "={{ \"name = '\" + $json.archive_name + \"' and 'YOUR_DRIVE_FOLDER_ID' in parents and trashed = false\" }}",
        "returnAll": false,
        "limit": 1,
        "filter": {
          "folderId": {
            "__rl": true,
            "value": "root",
            "mode": "list"
          }
        },
        "options": {}
      },
      "position": [
        420,
        260
      ],
      "alwaysOutputData": true
    },
    {
      "id": "76a5580d-1bdb-42a1-aa09-71e5b703879b",
      "name": "Original is archived",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "c26eeb32-31ba-479f-a8ff-ab4d11bbdbd6",
              "leftValue": "={{ String($json.id || \"\") }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "position": [
        650,
        260
      ]
    },
    {
      "id": "03095690-9293-4fd9-95c4-00b7505bdf14",
      "name": "Restore for archive",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "const original = $('Restore attachment').item;\nreturn [{ json: original.json, binary: original.binary }];"
      },
      "position": [
        870,
        400
      ]
    },
    {
      "id": "99d67696-a4c3-47ee-b2ba-2c5afdd96397",
      "name": "Archive original",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "parameters": {
        "resource": "file",
        "operation": "upload",
        "inputDataFieldName": "invoice",
        "name": "={{ $json.archive_name }}",
        "driveId": {
          "__rl": true,
          "value": "My Drive",
          "mode": "list"
        },
        "folderId": {
          "__rl": true,
          "value": "YOUR_DRIVE_FOLDER_ID",
          "mode": "id"
        },
        "options": {}
      },
      "position": [
        1090,
        400
      ]
    },
    {
      "id": "d474338d-5355-4ebb-80ec-e2e995f3ced7",
      "name": "Prepare extraction",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "const original = $('Restore attachment').item;\nconst id = $input.first().json.id;\nif (!id) throw new Error('Missing archived original id');\nreturn [{ json: { ...original.json, original_url: 'https://drive.google.com/file/d/' + id + '/view' },\n  binary: original.binary }];"
      },
      "position": [
        1320,
        260
      ]
    },
    {
      "id": "fd553360-f1fb-4288-af51-83d3396bb6dc",
      "name": "Respect page rate",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "parameters": {
        "amount": 15,
        "unit": "seconds"
      },
      "position": [
        1560,
        260
      ],
      "webhookId": "d560da97-0149-4e33-96a3-f81e336ca986"
    },
    {
      "id": "ed22ed00-6606-4f77-9af0-9915f88698d2",
      "name": "Extract invoice fields",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "parameters": {
        "method": "POST",
        "url": "https://repondea.com/api/invoice-csv/extract",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "={{ $binary.invoice.mimeType }}"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "binaryData",
        "inputDataFieldName": "invoice",
        "options": {
          "timeout": 90000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "position": [
        1800,
        260
      ]
    },
    {
      "id": "5e8b7890-4160-4d5f-b036-5393938c0c89",
      "name": "Prepare review row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "const result = $input.first().json;\nconst original = $('Prepare extraction').item.json;\nif (result.requires_review !== true || !result.fields || !Array.isArray(result.warnings))\n  throw new Error('Unexpected extraction result; message stays unread');\nconst text = value => String(value == null ? '' : value);\nconst row = { source_key: original.source_key };\nfor (const field of ['supplier','invoice_number','invoice_date','due_date','currency','net','tax','total'])\n  row[field] = text(result.fields[field]);\nrow.review_status = 'TO_REVIEW';\nrow.warnings = result.warnings.map(text).join(' | ');\nrow.original_url = original.original_url;\nrow.gmail_url = original.gmail_url;\nreturn [{ json: row }];"
      },
      "position": [
        2040,
        260
      ]
    },
    {
      "id": "3d13c181-7c92-4245-8aa0-8d0f482ef3bf",
      "name": "Write review queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "YOUR_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Invoices",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "source_key"
          ],
          "schema": [
            {
              "id": "source_key",
              "displayName": "source_key",
              "required": true,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "supplier",
              "displayName": "supplier",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "invoice_number",
              "displayName": "invoice_number",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "invoice_date",
              "displayName": "invoice_date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "due_date",
              "displayName": "due_date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "currency",
              "displayName": "currency",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "net",
              "displayName": "net",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "tax",
              "displayName": "tax",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "total",
              "displayName": "total",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "review_status",
              "displayName": "review_status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "warnings",
              "displayName": "warnings",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "original_url",
              "displayName": "original_url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "gmail_url",
              "displayName": "gmail_url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            }
          ]
        },
        "options": {
          "cellFormat": "RAW",
          "handlingExtraData": "ignoreIt"
        }
      },
      "position": [
        2280,
        260
      ]
    },
    {
      "id": "87cbbbef-99d3-4b74-a96a-997e7b9d2ddd",
      "name": "Mark email read after all rows",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "parameters": {
        "resource": "message",
        "operation": "markAsRead",
        "messageId": "={{ $(\"Gmail invoices\").first().json.id }}"
      },
      "position": [
        -250,
        -100
      ],
      "executeOnce": true
    },
    {
      "id": "7b07a484-0382-4d4d-8b36-951d9f123919",
      "name": "Setup — Gmail to a review queue",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Gmail → private Drive archive → Sheets review queue\nCreate a Gmail label Repondea-Factures. Apply it only to supplier invoice emails; create a Gmail filter for known suppliers if needed. The trigger fetches ONE unread message per poll and downloads attachments. Every supported attachment is processed sequentially. The message is marked read ONLY after all rows succeed. Processing errors stop the execution; inspect the error and retry the failed execution. Trigger backfill is not guaranteed: mark the message unread and run the trigger test explicitly when recovering an older message.\n\n## Configure once\n1. Connect Gmail, Google Drive and Google Sheets OAuth credentials to their nodes.\n2. Create a PRIVATE Drive folder. Replace YOUR_DRIVE_FOLDER_ID in the search query AND archive node.\n3. Create a spreadsheet tab Invoices with these exact headers:\nsource_key, supplier, invoice_number, invoice_date, due_date, currency, net, tax, total, review_status, warnings, original_url, gmail_url\n4. Replace YOUR_SHEET_ID in BOTH Sheets nodes.\n5. Create a verified Repondea account, generate a key, and select a Header Auth credential (Authorization: Bearer YOUR_KEY) on Extract invoice fields. Five pages/month free, optional 50 pages for €9/month. No model API key.\n6. Test with a fictional labelled invoice email. Check the original link, every field and TO_REVIEW row before activating. Serialize executions (n8n concurrency=1) to prevent simultaneous searches/appends.\n\n## Review and recovery\nExisting source_key rows are skipped; manual corrections/statuses are preserved. The archive search reuses an existing original on retry. This prevents replay duplicates for the SAME attachment in sequential runs; forwarded copies of the same invoice get different source keys and need manual duplicate checking. Failure after extraction but before Sheets can consume pages again on retry. No automatic retries of paid extraction. 15 seconds between reads avoids the 5/minute API limit.\nCheck TO_REVIEW rows against their original, correct them and set review_status=REVIEWED. Export only REVIEWED rows. RAW cell writes keep supplier text from executing spreadsheet formulas. Do not blindly open a CSV from untrusted fields without importing as text.\n\n## Scope and privacy\nPDF (1–3 pages), PNG/JPEG, ≤5 MB; one invoice per attachment. No accounting entry, bank transfer or tax filing. Originals remain in your private Drive, fields in your Sheet and n8n history; configure execution retention. Repondea retains only usage/account data. Google credentials stay in your n8n instance. This is a configurable workflow, not a hosted Gmail connector or a guaranteed accounting integration.\n\n[Guide and download](https://repondea.com/en/invoice-csv/n8n/gmail?source=n8n-gmail-invoice-csv)",
        "height": 1500,
        "width": 780,
        "color": 1
      },
      "position": [
        -1100,
        -1800
      ]
    },
    {
      "id": "fed3e123-3822-4ba7-b41f-77b8e7cc7124",
      "name": "How the inbox is processed",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Email to a review queue, not automatic payment\nThe Gmail trigger watches your invoice label. Attachments are split and processed one at a time. Messages with no supported invoice attachment stop before being marked read; other files in mixed messages are ignored. Configure the label, Google OAuth credentials, private Drive folder and Sheet IDs first.\n\nThe account/key and service limits are documented in the yellow setup note. Test a fictional invoice before activation. The last Gmail node marks the email read only after every supported attachment reaches the review queue.\n\n[Full setup guide](https://repondea.com/en/invoice-csv/n8n/gmail?source=n8n-gmail-invoice-csv)",
        "height": 700,
        "width": 600,
        "color": 4
      },
      "position": [
        -100,
        -1100
      ]
    },
    {
      "id": "79ae424f-371d-4902-a1f4-0233761a53e4",
      "name": "Failure recovery and duplicate scope",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Inspect failures, then recover\nA failed execution stops before the email is marked read. Search the review queue by source_key; preserve completed rows and manual edits. Search Drive by that same key before uploading an original again.\n\nRun executions sequentially (concurrency=1). Recovery of an old email can require marking it unread and explicitly testing the trigger: backfill is not guaranteed.\n\nThese lookups prevent sequential replay of the SAME message attachment. They do not identify forwarded copies of the same invoice. Check supplier + invoice number during human review. Extraction succeeded but Sheets failed? Retrying can consume the extraction quota again. No automatic retry of extraction is enabled.",
        "height": 750,
        "width": 720,
        "color": 3
      },
      "position": [
        800,
        -1100
      ]
    },
    {
      "id": "206273f3-b921-4b8e-99d0-f65460d53abd",
      "name": "Review originals before export",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## TO_REVIEW is never a payment instruction\nOpen original_url, check each field, correct mistakes and mark the row REVIEWED. Export only reviewed rows. Google credentials stay in your n8n instance; originals stay in your private Drive.\n\nPDF 1–3 pages or PNG/JPEG, at most 5 MB, one invoice per attachment. Five pages per month free; optional 50 pages for EUR 9/month. No bank payment, accounting posting or tax filing.\n\n### Optional: extracting a single upload\nThe video below covers our separate, simpler upload workflow (account, API key and field review). It does not demonstrate Gmail/Drive/Sheets setup. Use the full guide above for this template.\n@[youtube](HafeDzkdsBg)",
        "height": 1000,
        "width": 800,
        "color": 6
      },
      "position": [
        1780,
        -1350
      ]
    }
  ],
  "connections": {
    "Gmail invoices": {
      "main": [
        [
          {
            "node": "Split invoice attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split invoice attachments": {
      "main": [
        [
          {
            "node": "One invoice at a time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One invoice at a time": {
      "main": [
        [
          {
            "node": "Mark email read after all rows",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Find existing row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find existing row": {
      "main": [
        [
          {
            "node": "Already in queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Already in queue": {
      "main": [
        [
          {
            "node": "One invoice at a time",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Restore attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore attachment": {
      "main": [
        [
          {
            "node": "Find archived original",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find archived original": {
      "main": [
        [
          {
            "node": "Original is archived",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Original is archived": {
      "main": [
        [
          {
            "node": "Prepare extraction",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Restore for archive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restore for archive": {
      "main": [
        [
          {
            "node": "Archive original",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Archive original": {
      "main": [
        [
          {
            "node": "Prepare extraction",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare extraction": {
      "main": [
        [
          {
            "node": "Respect page rate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respect page rate": {
      "main": [
        [
          {
            "node": "Extract invoice fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract invoice fields": {
      "main": [
        [
          {
            "node": "Prepare review row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare review row": {
      "main": [
        [
          {
            "node": "Write review queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write review queue": {
      "main": [
        [
          {
            "node": "One invoice at a time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "pinData": {},
  "tags": []
}
