{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "39652963-fcf3-4a38-a624-494fff9c0ad2",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3248,
        -304
      ],
      "parameters": {
        "color": 7,
        "width": 380,
        "height": 1100,
        "content": "## QuickBooks: AI Invoice Processor\nAutomatically processes vendor invoices received by email.\n\n### Who is this for?\n- Small/medium businesses using QuickBooks Online\n- Bookkeepers processing 20+ invoices/month\n- Accounting firms managing multiple clients\n\n### How it works\n1. **Gmail** monitors for new emails with PDF attachments\n2. **Extract from PDF** converts the PDF to text\n3. **AI (Information Extractor)** extracts structured invoice data and classifies if it's actually an invoice\n4. **QuickBooks** vendor search by name \u2192 gets Vendor ID\n5. **QuickBooks** creates a Bill with extracted data\n6. **PDF attachment** pipeline uploads the original invoice to the bill\n7. **Confirmation email** sent back to the sender\n\n### Setup checklist\n- [ ] Connect **Gmail** credentials\n- [ ] Connect **OpenAI** API credentials\n- [ ] Connect **QuickBooks OAuth2** credentials\n- [ ] Edit the **Config** node with your values:\n  - `realmId` \u2014 your QuickBooks Company ID\n  - `apTeamEmail` \u2014 AP team email for error notifications\n  - `defaultExpenseAccountId` \u2014 see steps below\n- [ ] (Optional) Add Gmail label filter\n\n### How to find `defaultExpenseAccountId`\n1. Log in to **QuickBooks Online**\n2. Go to **Settings** (gear icon) \u2192 **Chart of Accounts**\n3. Find your default expense account (e.g. \"Accounts Payable\", \"Office Supplies\", \"Professional Services\")\n4. Click on the account \u2192 look at the URL: `...?accountId=`**`123`**\n5. Copy the number and paste it into the **Config** node"
      },
      "typeVersion": 1
    },
    {
      "id": "80ffeb80-2ad7-4f75-81e8-b2f0ef11a3da",
      "name": "Gmail Trigger",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        -2624,
        464
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "q": "filename:pdf"
        },
        "options": {
          "downloadAttachments": true
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyX",
              "unit": "minutes",
              "value": 15
            }
          ]
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "831e4ca0-e94d-4aec-9938-351a14fff163",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "position": [
        -2400,
        464
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "realm-id",
              "name": "realmId",
              "type": "string",
              "value": "ADD_REALM_ID"
            },
            {
              "id": "ap-email",
              "name": "apTeamEmail",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "confirm-enabled",
              "name": "sendConfirmation",
              "type": "boolean",
              "value": false
            },
            {
              "id": "expense-account",
              "name": "defaultExpenseAccountId",
              "type": "string",
              "value": "ADD_EXPENSE_ACC_ID"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "57b2333f-236c-45fa-858f-075e0b5f61ae",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2704,
        -304
      ],
      "parameters": {
        "color": 4,
        "width": 540,
        "height": 340,
        "content": "## 1. Email Trigger & Config\nMonitors Gmail for new emails with PDF invoice attachments.\n\n**Config node** centralizes all settings:\n- `realmId` \u2014 Your QuickBooks Company ID (find in QB \u2192 Settings \u2192 Account)\n- `apTeamEmail` \u2014 Where error notifications go\n- `sendConfirmation` \u2014 Toggle confirmation emails on/off\n- `defaultExpenseAccountId` \u2014 QB expense account for line items\n\n**Gmail Setup:**\n- Connect your Gmail account\n- Optionally filter by label or sender\n- `Simplify` is set to false to get headers + binary attachments"
      },
      "typeVersion": 1
    },
    {
      "id": "d0090bee-8ac3-4800-9109-d8ab7becd36b",
      "name": "Extract Text from PDF",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -2176,
        464
      ],
      "parameters": {
        "options": {
          "joinPages": true
        },
        "operation": "pdf",
        "binaryPropertyName": "attachment_0"
      },
      "typeVersion": 1
    },
    {
      "id": "1922f9d5-77cb-4d7f-be4c-2c3c7ba3fb1e",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1792,
        -320
      ],
      "parameters": {
        "color": 6,
        "width": 520,
        "height": 460,
        "content": "## 2. AI Invoice Classification & Extraction\nUses the **Information Extractor** node with OpenAI to:\n\n1. **Classify** if the PDF is actually an invoice (`is_invoice`)\n2. **Extract** structured data if it is:\n   - `vendor_name` \u2014 Company issuing the invoice\n   - `invoice_number` \u2014 Reference number\n   - `amount` \u2014 Total due\n   - `currency` \u2014 3-letter code (USD, EUR, etc.)\n   - `due_date` / `txn_date` \u2014 Dates in YYYY-MM-DD\n   - `line_items` \u2014 Array of {description, amount, quantity}\n\nNon-invoices (receipts, contracts, etc.) are silently skipped.\nInvalid extractions (missing vendor/amount) route to AP team."
      },
      "typeVersion": 1
    },
    {
      "id": "25001adb-7534-4591-b2c9-d281ce45dbb9",
      "name": "AI Extract Invoice Data",
      "type": "@n8n/n8n-nodes-langchain.informationExtractor",
      "position": [
        -1952,
        464
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "options": {},
        "schemaType": "fromJson",
        "jsonSchemaExample": "{\n  \"is_invoice\": true,\n  \"vendor_name\": \"Acme Corp\",\n  \"invoice_number\": \"INV-2024-001\",\n  \"amount\": 1500.00,\n  \"currency\": \"USD\",\n  \"due_date\": \"2024-12-31\",\n  \"txn_date\": \"2024-12-01\",\n  \"line_items\": [\n    {\n      \"description\": \"Consulting services\",\n      \"amount\": 1500.00,\n      \"quantity\": 1\n    }\n  ]\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "630af547-c6aa-4970-8b25-6e15cde0ec8f",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -1888,
        688
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "gpt-4o"
        },
        "options": {
          "temperature": 0
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "b4aed971-35b5-49dd-abc3-7b3b7a3efa13",
      "name": "Is Invoice?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1600,
        464
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "invoice-check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.output.is_invoice }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "abf0afc3-ff40-44b4-8333-1b81d1ce2ca9",
      "name": "Validate Extracted Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -1376,
        464
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Validate required fields from AI extraction\nconst data = $json.output;\nconst errors = [];\n\nif (!data.vendor_name) errors.push('vendor_name is missing');\nif (!data.amount || data.amount <= 0) errors.push('amount is missing or invalid');\nif (!data.line_items || data.line_items.length === 0) errors.push('no line items found');\n\n// Normalize dates\nconst normalizeDate = (d) => {\n  if (!d) return null;\n  const parsed = new Date(d);\n  if (isNaN(parsed.getTime())) return null;\n  return parsed.toISOString().split('T')[0];\n};\n\nreturn {\n  json: {\n    valid: errors.length === 0,\n    errors: errors,\n    data: {\n      ...data,\n      due_date: normalizeDate(data.due_date),\n      txn_date: normalizeDate(data.txn_date),\n      currency: data.currency || 'USD'\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "696a8869-5b7b-494f-a438-ab4a8f096b01",
      "name": "Is Valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1152,
        464
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "valid-check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.valid }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f4a8ba4c-523a-4080-a8f0-5ca820bd32fe",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -944,
        -336
      ],
      "parameters": {
        "color": 3,
        "width": 600,
        "height": 464,
        "content": "## 3. QuickBooks: Vendor Search & Bill Creation\nSearches QuickBooks for the vendor by name extracted by AI.\n- If no vendor found \u2192 error notification to AP team\n\n**Bill Creation (Native QB node):**\n- Uses native QuickBooks node for reliable OAuth2 auth\n- Single line item with total amount (native node limitation)\n- Invoice number included in line description\n- DueDate, TxnDate, TotalAmt from AI extraction\n- Returns Bill ID for PDF attachment step\n\n**Note:** Native QB node supports only one line item per bill.\nIndividual line items are combined into the description field."
      },
      "typeVersion": 1
    },
    {
      "id": "6a3cf6b3-4f64-45ab-872c-e3fb4b81893f",
      "name": "Vendor Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        -704,
        368
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "vendor-exists",
              "operator": {
                "type": "string",
                "operation": "exists"
              },
              "leftValue": "={{ $json.Id }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "7df64e49-0f8a-4b0d-ba1f-4d879b89ecbb",
      "name": "Prepare Bill Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -480,
        272
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const data = $('Validate Extracted Data').item.json.data;\nconst vendorId = $('Search vendor').item.json.Id;\nconst config = $('Config').item.json;\n\n// Build combined description from line items + invoice number\nconst lineDescriptions = (data.line_items || []).map(item => {\n  const qty = item.quantity && item.quantity > 1 ? ` x${item.quantity}` : '';\n  return `${item.description}${qty} - ${data.currency} ${item.amount}`;\n});\n\nlet description = '';\nif (data.invoice_number) description += `Invoice #${data.invoice_number}\\n`;\ndescription += lineDescriptions.join('\\n');\n\nreturn {\n  json: {\n    vendorId: vendorId,\n    amount: data.amount,\n    description: description.substring(0, 4000),\n    accountId: config.defaultExpenseAccountId,\n    dueDate: data.due_date || '',\n    txnDate: data.txn_date || '',\n    totalAmt: data.amount\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "f1ec4648-a509-4f54-aeb3-4102c663d1a1",
      "name": "Create Bill",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        -256,
        272
      ],
      "parameters": {
        "Line": [
          {
            "Amount": "={{ $json.amount }}",
            "accountId": "={{ $json.accountId }}",
            "DetailType": "AccountBasedExpenseLineDetail",
            "Description": "={{ $json.description }}"
          }
        ],
        "resource": "bill",
        "VendorRef": "={{ $json.vendorId }}",
        "operation": "create",
        "additionalFields": {
          "DueDate": "={{ $json.dueDate }}",
          "TxnDate": "={{ $json.txnDate }}",
          "TotalAmt": "={{ $json.totalAmt }}"
        }
      },
      "credentials": {
        "quickBooksOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7db34b32-3a46-40a8-b0c3-8a139035fd11",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        -336
      ],
      "parameters": {
        "color": 2,
        "width": 1004,
        "height": 464,
        "content": "## 4. PDF Attachment Pipeline\nAttaches the original PDF invoice to the created QuickBooks bill.\n\n**Binary data flow:**\nBinary (PDF) is lost after the AI extraction step, so the attachment nodes reference it by name from **Config** using `$('Config').item.binary.attachment_0`.\n\nBoth nodes are connected to Create Bill for **execution order** (they must run after the bill exists), but get binary data via named reference.\n\n**The Force Inline Binary fix:**\nn8n v2 stores binary as DB streams without Content-Length. QuickBooks upload API requires it. The Code node converts streams to inline base64.\n\n**Pipeline:**\n1. Build metadata JSON (AttachableRef \u2192 Bill) \u2014 gets binary from Config by name\n2. Convert metadata to binary field (`myBinaryFile`)\n3. Fetch PDF Binary from Config by name\n4. Merge PDF binary + metadata binary into one item\n5. \u26a0\ufe0f Force Inline Binary (stream \u2192 base64)\n6. Upload multipart to QB `/upload` endpoint"
      },
      "typeVersion": 1
    },
    {
      "id": "fd3ac551-c596-4e60-a49c-ef28e138824d",
      "name": "Build Attachment Metadata",
      "type": "n8n-nodes-base.code",
      "position": [
        -32,
        176
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Binary is lost in the main pipeline after AI extraction.\n// Reference it by name from Config node which still has attachment_0.\nconst binaryData = $('Config').item.binary.attachment_0;\n\nreturn {\n  json: {\n    \"AttachableRef\": [{\n      \"EntityRef\": {\n        \"type\": \"Bill\",\n        \"value\": $('Create Bill').item.json.Id\n      }\n    }],\n    \"FileName\": binaryData.fileName,\n    \"ContentType\": binaryData.mimeType\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c28826ab-acca-4d7e-86ed-ba02cf0b2f72",
      "name": "Metadata to Binary",
      "type": "n8n-nodes-base.code",
      "position": [
        192,
        176
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const jsonString = JSON.stringify($json);\n  const buffer = Buffer.from(jsonString, 'utf-8');\n\nreturn {\n    json: $json,\n    binary: {          \n      myBinaryFile: {\n        data: buffer.toString('base64'),\n        mimeType: 'application/json',\n        fileName: 'data.json'\n      }\n    }\n  };"
      },
      "typeVersion": 2
    },
    {
      "id": "18017675-b796-4d38-b5da-219a7baa10ec",
      "name": "Fetch PDF Binary",
      "type": "n8n-nodes-base.code",
      "position": [
        192,
        368
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Fetch the original PDF binary from Config node by name.\n// Binary is lost in the main pipeline after AI extraction,\n// but Config still has attachment_0.\nconst configBinary = $('Config').item.binary;\n\nreturn {\n  json: {},\n  binary: configBinary\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "07470c1c-506a-4499-a9d7-7b481db3af7e",
      "name": "Merge PDF + Metadata",
      "type": "n8n-nodes-base.merge",
      "position": [
        416,
        272
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "c997f004-ac58-4128-9bc2-f74ba73e8573",
      "name": "Force Inline Binary",
      "type": "n8n-nodes-base.code",
      "position": [
        640,
        272
      ],
      "parameters": {
        "jsCode": "// CRITICAL: Force binary data from database stream to inline base64.\n// Without this, QuickBooks upload API fails because n8n v2 stores\n// binary as DB streams which lack Content-Length headers.\n\nconst items = $input.all();\n\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n\n  if (item.binary?.attachment_0) {\n    const buffer = await this.helpers.getBinaryDataBuffer(i, 'attachment_0');\n    const meta = item.binary.attachment_0;\n    item.binary.attachment_0 = {\n      data: buffer.toString('base64'),\n      mimeType: meta.mimeType,\n      fileName: meta.fileName,\n      fileExtension: meta.fileExtension || 'pdf',\n    };\n  }\n\n  if (item.binary?.myBinaryFile) {\n    const buffer = await this.helpers.getBinaryDataBuffer(i, 'myBinaryFile');\n    const meta = item.binary.myBinaryFile;\n    item.binary.myBinaryFile = {\n      data: buffer.toString('base64'),\n      mimeType: meta.mimeType,\n      fileName: meta.fileName,\n      fileExtension: meta.fileExtension || 'json',\n    };\n  }\n}\n\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "9acd7686-2d20-4411-97e7-1c070a69d49e",
      "name": "Upload PDF to Bill",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        272
      ],
      "parameters": {
        "url": "=https://quickbooks.api.intuit.com/v3/company/{{ $('Config').item.json.realmId }}/upload",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          }
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "file_metadata_01",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "=myBinaryFile"
            },
            {
              "name": "file_content_01",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "attachment_0"
            }
          ]
        },
        "nodeCredentialType": "quickBooksOAuth2Api"
      },
      "credentials": {
        "quickBooksOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "4943de3b-0962-4410-afbb-5f1b1e9b7b34",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1040,
        -336
      ],
      "parameters": {
        "color": 5,
        "width": 400,
        "height": 420,
        "content": "## 5. Confirmation & Error Handling\n\n**Success path:**\nSends confirmation email back to the invoice sender with bill details (toggleable via Config).\n\n**Skip paths:**\n- \u23ed\ufe0f **Not an invoice** \u2014 PDF is not an invoice, silently skipped\n\n**Error paths:**\n- \u274c **AI extraction failed** \u2014 Missing required fields, forwards to AP team\n- \u274c **Vendor not found** \u2014 Notifies AP team with the vendor name so they can create it in QuickBooks"
      },
      "typeVersion": 1
    },
    {
      "id": "6f3b3896-bfb0-4d1d-b044-764ad7ab9079",
      "name": "Send Confirmation?",
      "type": "n8n-nodes-base.if",
      "position": [
        1088,
        272
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "confirm-check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('Config').item.json.sendConfirmation }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c4de8abb-56db-471a-b4ff-85f9193a79b4",
      "name": "Send Confirmation Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1312,
        272
      ],
      "parameters": {
        "sendTo": "={{ $('Gmail Trigger').item.json.from }}",
        "message": "=Hi,\n\nYour invoice has been processed automatically.\n\u2022 PDF: Attached to bill \u2713\n\nThis is an automated message from the Invoice Processor workflow.",
        "options": {},
        "subject": "=Invoice Processed: {{ $('Validate Extracted Data').item.json.data.invoice_number || 'N/A' }} \u2192 QuickBooks Bill #{{ $('Create Bill').item.json.Id }}",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "2f320970-0c12-4dec-bfc4-5ca4feaef46a",
      "name": "Error: Extraction Failed",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -928,
        560
      ],
      "parameters": {
        "sendTo": "={{ $('Config').item.json.apTeamEmail }}",
        "message": "=The AI could not extract valid invoice data from an email.\n\n\u2022 From: {{ $('Gmail Trigger').item.json.from }}\n\u2022 Subject: {{ $('Gmail Trigger').item.json.subject }}\n\u2022 Errors: {{ $json.errors.join(', ') }}\n\nPlease process this invoice manually in QuickBooks.\n\nOriginal email ID: {{ $('Gmail Trigger').item.json.messageId }}",
        "options": {},
        "subject": "=[Manual Processing] Could not extract invoice data from {{ $('Gmail Trigger').item.json.from }}",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "ce12f7ea-c81b-4751-9e13-39f11e53ddc6",
      "name": "Error: Vendor Not Found",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -480,
        464
      ],
      "parameters": {
        "sendTo": "={{ $('Config').item.json.apTeamEmail }}",
        "message": "=An invoice was received but the vendor was not found in QuickBooks.\n\n\u2022 Vendor Name (from invoice): {{ $('Validate Extracted Data').item.json.data.vendor_name }}\n\u2022 Invoice #: {{ $('Validate Extracted Data').item.json.data.invoice_number || 'N/A' }}\n\u2022 Amount: {{ $('Validate Extracted Data').item.json.data.currency }} {{ $('Validate Extracted Data').item.json.data.amount }}\n\u2022 From: {{ $('Gmail Trigger').item.json.from }}\n\n**Action needed:**\n1. Create the vendor in QuickBooks, OR\n2. Check if the vendor exists under a different name\n3. Process the invoice manually\n\nOriginal email ID: {{ $('Gmail Trigger').item.json.messageId }}",
        "options": {},
        "subject": "=[Action Required] Unknown vendor: {{ $('Validate Extracted Data').item.json.data.vendor_name }}",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "da38272a-a472-4af7-ac6c-a7e490d128b4",
      "name": "Search vendor",
      "type": "n8n-nodes-base.quickbooks",
      "position": [
        -928,
        368
      ],
      "parameters": {
        "limit": 1,
        "filters": {
          "query": "=WHERE DisplayName LIKE '%{{ $json.data.vendor_name }}%'"
        },
        "resource": "vendor",
        "operation": "getAll"
      },
      "credentials": {
        "quickBooksOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Config": {
      "main": [
        [
          {
            "node": "Extract Text from PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Valid?": {
      "main": [
        [
          {
            "node": "Search vendor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error: Extraction Failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Bill": {
      "main": [
        [
          {
            "node": "Build Attachment Metadata",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch PDF Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Invoice?": {
      "main": [
        [
          {
            "node": "Validate Extracted Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail Trigger": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search vendor": {
      "main": [
        [
          {
            "node": "Vendor Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vendor Found?": {
      "main": [
        [
          {
            "node": "Prepare Bill Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error: Vendor Not Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch PDF Binary": {
      "main": [
        [
          {
            "node": "Merge PDF + Metadata",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Extract Invoice Data",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Bill Data": {
      "main": [
        [
          {
            "node": "Create Bill",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Metadata to Binary": {
      "main": [
        [
          {
            "node": "Merge PDF + Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Confirmation?": {
      "main": [
        [
          {
            "node": "Send Confirmation Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload PDF to Bill": {
      "main": [
        [
          {
            "node": "Send Confirmation?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Force Inline Binary": {
      "main": [
        [
          {
            "node": "Upload PDF to Bill",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge PDF + Metadata": {
      "main": [
        [
          {
            "node": "Force Inline Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Text from PDF": {
      "main": [
        [
          {
            "node": "AI Extract Invoice Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Extract Invoice Data": {
      "main": [
        [
          {
            "node": "Is Invoice?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Extracted Data": {
      "main": [
        [
          {
            "node": "Is Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Attachment Metadata": {
      "main": [
        [
          {
            "node": "Metadata to Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}