{
  "name": "Lumeor | AI invoice processing (Gmail to QuickBooks)",
  "nodes": [
    {
      "parameters": {
        "content": "## AI invoice processing (Gmail to QuickBooks Online)\n\n**What it does**\n1. Watches the AP inbox for new emails with a PDF or image attachment.\n2. Claude reads the invoice and returns strict JSON (structured outputs).\n3. Code checks the arithmetic, dates, currency and document type.\n4. Looks up the vendor and blocks duplicate invoice numbers in QuickBooks Online.\n5. Clean invoices under your threshold become bills. Everything else goes to Slack with the reason.\n\n**Setup**\n1. Credentials: Gmail OAuth2, Anthropic (n8n's built-in Anthropic credential), QuickBooks Online OAuth2, Slack.\n2. In **Gmail Trigger**, replace REPLACE_WITH_AP_INBOX_ADDRESS.\n3. In **Config**, set qboRealmId (your QuickBooks company ID), expenseAccountId and approvalThreshold. Use https://sandbox-quickbooks.api.intuit.com while testing.\n4. In **Post to review channel**, replace REPLACE_WITH_SLACK_CHANNEL.\n5. Run on a few real invoices with the bill step disabled before going live.\n\nModel: claude-opus-5-5. For high-volume, simple documents, claude-haiku-5-5 is a cheaper option.\n\nTemplate by Lumeor Studio, https://www.lumeor.studio/guides/automate-invoice-processing-with-ai/",
        "height": 720,
        "width": 520
      },
      "id": "94a77a62-f772-44af-8c1a-6af7e32a8ee0",
      "name": "Template notes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -620,
        -260
      ]
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "to:REPLACE_WITH_AP_INBOX_ADDRESS has:attachment {filename:pdf filename:jpg filename:jpeg filename:png}",
          "readStatus": "unread"
        },
        "options": {
          "downloadAttachments": true,
          "dataPropertyAttachmentsPrefixName": "attachment_"
        }
      },
      "id": "1299e85c-54f3-4951-87d5-93dc1089a116",
      "name": "Gmail Trigger",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "3473e27d-d530-4ed2-ac31-e1a5e3108925",
              "name": "qboBaseUrl",
              "value": "https://quickbooks.api.intuit.com",
              "type": "string"
            },
            {
              "id": "abf41b66-e6a4-492a-817f-4913cb4a6a14",
              "name": "qboRealmId",
              "value": "REPLACE_WITH_QBO_COMPANY_ID",
              "type": "string"
            },
            {
              "id": "267005ab-a1c3-4773-af17-38ed1872354e",
              "name": "expenseAccountId",
              "value": "REPLACE_WITH_QBO_EXPENSE_ACCOUNT_ID",
              "type": "string"
            },
            {
              "id": "f2044462-c57f-4c82-9e4b-8053c4f31c36",
              "name": "approvalThreshold",
              "value": 2500,
              "type": "number"
            },
            {
              "id": "e845dfe1-d407-4d37-8c0c-29f8017af16e",
              "name": "amountTolerance",
              "value": 0.02,
              "type": "number"
            },
            {
              "id": "2811c82f-fb80-4e48-be44-3e3c6a542f82",
              "name": "homeCurrency",
              "value": "USD",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {
          "stripBinary": false
        }
      },
      "id": "38bf973a-b2ae-484a-99ce-98b54c0f8901",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Turns each invoice email into a Claude extraction request.\n// Reads the first PDF or image attachment and sends it as a base64 content block.\nconst SUPPORTED = {\n  'application/pdf': 'document',\n  'image/jpeg': 'image',\n  'image/png': 'image',\n  'image/gif': 'image',\n  'image/webp': 'image',\n};\n\nconst lineItem = {\n  type: 'object',\n  additionalProperties: false,\n  required: ['description', 'quantity', 'unit_price', 'amount'],\n  properties: {\n    description: { type: 'string' },\n    quantity: { type: 'number' },\n    unit_price: { type: 'number' },\n    amount: { type: 'number' },\n  },\n};\n\nconst schema = {\n  type: 'object',\n  additionalProperties: false,\n  required: ['document_type', 'multiple_invoices', 'vendor_name', 'vendor_tax_id', 'invoice_number', 'invoice_date', 'due_date', 'po_number', 'currency', 'subtotal', 'tax', 'total', 'line_items'],\n  properties: {\n    document_type: { type: 'string', enum: ['invoice', 'credit_memo', 'statement', 'receipt', 'other'] },\n    multiple_invoices: { type: 'boolean', description: 'True if the file contains more than one invoice' },\n    vendor_name: { type: 'string' },\n    vendor_tax_id: { type: 'string', description: 'Empty string if not printed' },\n    invoice_number: { type: 'string' },\n    invoice_date: { type: 'string', description: 'YYYY-MM-DD' },\n    due_date: { type: 'string', description: 'YYYY-MM-DD, or empty string if not printed' },\n    po_number: { type: 'string', description: 'Empty string if not printed' },\n    currency: { type: 'string', description: 'ISO 4217 code such as USD' },\n    subtotal: { type: 'number' },\n    tax: { type: 'number', description: '0 if no tax is shown' },\n    total: { type: 'number' },\n    line_items: { type: 'array', items: lineItem },\n  },\n};\n\nconst PROMPT = 'Extract the fields of this vendor invoice. Copy values exactly as printed. ' +\n  'Use YYYY-MM-DD dates and plain numbers without currency symbols. ' +\n  'If a field is not printed, use an empty string (or 0 for tax). Do not guess or calculate missing values. ' +\n  'Classify the document type honestly: credit memos, statements and receipts are not invoices.';\n\nconst out = [];\nconst items = $input.all();\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  const email = item.json;\n  const binary = item.binary || {};\n  const key = Object.keys(binary).find((k) => SUPPORTED[binary[k].mimeType]);\n  if (!key) continue; // Gmail query only picks PDFs and images, so this is rare.\n\n  const mimeType = binary[key].mimeType;\n  const buffer = await this.helpers.getBinaryDataBuffer(i, key);\n  const source = { type: 'base64', media_type: mimeType, data: buffer.toString('base64') };\n  const fileBlock = SUPPORTED[mimeType] === 'document' ? { type: 'document', source } : { type: 'image', source };\n\n  out.push({\n    json: {\n      messageId: email.id,\n      from: (email.from && email.from.text) || '',\n      subject: email.subject || '',\n      attachmentName: binary[key].fileName || key,\n      claudeRequest: {\n        model: 'claude-opus-5-5',\n        max_tokens: 4000,\n        messages: [{ role: 'user', content: [fileBlock, { type: 'text', text: PROMPT }] }],\n        output_config: { format: { type: 'json_schema', schema } },\n      },\n    },\n    pairedItem: { item: i },\n  });\n}\nreturn out;\n"
      },
      "id": "64b7a1e0-2005-4eda-9c37-d707a957c5d4",
      "name": "Build extraction request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "anthropicApi",
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.claudeRequest) }}",
        "options": {
          "timeout": 180000
        }
      },
      "id": "0efc59f9-e50a-4e73-a06b-130a40470cd4",
      "name": "Claude: extract invoice",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Parses Claude's JSON and runs deterministic checks. Code, not the model, decides what is valid.\n\n// Reads a Messages API response. Thinking blocks may come first, so find the text block.\nfunction readClaude(resp) {\n  if (!resp || resp.error) {\n    const msg = resp && resp.error ? (resp.error.message || JSON.stringify(resp.error)) : 'empty response';\n    return { ok: false, reason: 'api_error: ' + String(msg).slice(0, 300) };\n  }\n  if (resp.stop_reason === 'refusal') {\n    const cat = resp.stop_details && resp.stop_details.category ? ' (' + resp.stop_details.category + ')' : '';\n    return { ok: false, reason: 'model_refusal' + cat };\n  }\n  if (resp.stop_reason === 'max_tokens') return { ok: false, reason: 'max_tokens: output was cut off, raise max_tokens' };\n  const block = (resp.content || []).find((b) => b.type === 'text');\n  if (!block) return { ok: false, reason: 'no_text_block_in_response' };\n  try {\n    return { ok: true, data: JSON.parse(block.text) };\n  } catch (e) {\n    return { ok: false, reason: 'invalid_json: ' + e.message };\n  }\n}\n\nconst cfg = $('Config').first().json;\nconst tol = Number(cfg.amountTolerance) || 0.02;\nconst round2 = (n) => Math.round(Number(n || 0) * 100) / 100;\nconst qboQuote = (s) => String(s || '').replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\nconst isDate = (s) => /^\\d{4}-\\d{2}-\\d{2}$/.test(s || '') && !isNaN(Date.parse(s));\n\nconst out = [];\nconst items = $input.all();\nfor (let i = 0; i < items.length; i++) {\n  const src = $('Build extraction request').itemMatching(i).json;\n  const base = { messageId: src.messageId, from: src.from, subject: src.subject, attachmentName: src.attachmentName };\n  const r = readClaude(items[i].json);\n  const reasons = [];\n  let inv = null;\n\n  if (!r.ok) {\n    reasons.push(r.reason);\n  } else {\n    inv = r.data;\n    if (inv.document_type !== 'invoice') reasons.push('not_an_invoice: ' + inv.document_type);\n    if (inv.multiple_invoices) reasons.push('multiple_invoices_in_one_file');\n    if (!inv.vendor_name) reasons.push('missing_vendor_name');\n    if (!inv.invoice_number) reasons.push('missing_invoice_number');\n    if (inv.invoice_number && inv.invoice_number.length > 21) reasons.push('invoice_number_longer_than_qbo_limit');\n    if (!isDate(inv.invoice_date)) reasons.push('bad_invoice_date');\n    else if (Date.parse(inv.invoice_date) > Date.now() + 86400000) reasons.push('invoice_date_in_future');\n    if (!(inv.total > 0)) reasons.push('total_not_positive');\n    if ((inv.currency || '').toUpperCase() !== String(cfg.homeCurrency).toUpperCase()) reasons.push('currency_' + inv.currency);\n    const lineSum = round2((inv.line_items || []).reduce((s, l) => s + Number(l.amount || 0), 0));\n    if (inv.line_items.length && Math.abs(lineSum - round2(inv.subtotal)) > tol) reasons.push('lines_' + lineSum + '_do_not_match_subtotal_' + inv.subtotal);\n    if (Math.abs(round2(inv.subtotal + inv.tax) - round2(inv.total)) > tol) reasons.push('subtotal_plus_tax_does_not_match_total');\n  }\n\n  const firstWord = inv ? (inv.vendor_name.split(/\\s+/).find((w) => w.replace(/[^A-Za-z0-9]/g, '').length >= 3) || inv.vendor_name) : '__none__';\n  out.push({\n    json: {\n      ...base,\n      invoice: inv,\n      reasons,\n      route: reasons.length ? 'review' : 'lookup',\n      vendorQuery: inv\n        ? \"select * from Vendor where DisplayName like '%\" + qboQuote(firstWord.replace(/[^A-Za-z0-9&' -]/g, '')) + \"%' maxresults 20\"\n        : \"select * from Vendor where Id = '0'\",\n      billQuery: \"select * from Bill where DocNumber = '\" + qboQuote(inv ? inv.invoice_number : '__none__') + \"' maxresults 20\",\n    },\n    pairedItem: { item: i },\n  });\n}\nreturn out;\n"
      },
      "id": "bb5b08e8-a441-4711-a5e6-26ba2b5c9902",
      "name": "Parse and validate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $('Config').first().json.qboBaseUrl }}/v3/company/{{ $('Config').first().json.qboRealmId }}/query",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "quickBooksOAuth2Api",
        "sendQuery": true,
        "specifyQuery": "keypair",
        "queryParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "={{ $json.vendorQuery }}"
            },
            {
              "name": "minorversion",
              "value": "75"
            }
          ]
        },
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {}
      },
      "id": "bf4a0db6-f409-4212-ba89-fb63aa6cb3f9",
      "name": "QBO: find vendor",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        0
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $('Config').first().json.qboBaseUrl }}/v3/company/{{ $('Config').first().json.qboRealmId }}/query",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "quickBooksOAuth2Api",
        "sendQuery": true,
        "specifyQuery": "keypair",
        "queryParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "={{ $('Parse and validate').item.json.billQuery }}"
            },
            {
              "name": "minorversion",
              "value": "75"
            }
          ]
        },
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {}
      },
      "id": "233ff454-6d6b-4170-aeed-dfdd8fbfab45",
      "name": "QBO: find existing bill",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        0
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Matches the vendor, blocks duplicates, applies the approval threshold and builds the QuickBooks bill.\nconst cfg = $('Config').first().json;\nconst round2 = (n) => Math.round(Number(n || 0) * 100) / 100;\nconst norm = (s) => String(s || '').toLowerCase()\n  .replace(/[^a-z0-9 ]/g, ' ')\n  .replace(/\\b(inc|llc|ltd|corp|corporation|co|company|the)\\b/g, ' ')\n  .replace(/\\s+/g, ' ').trim();\n\nconst out = [];\nconst items = $input.all();\nfor (let i = 0; i < items.length; i++) {\n  const p = $('Parse and validate').itemMatching(i).json;\n  const vendorResp = $('QBO: find vendor').itemMatching(i).json;\n  const billResp = items[i].json;\n  const reasons = [...p.reasons];\n  const inv = p.invoice;\n  let vendor = null;\n  let qboBill = null;\n\n  if (p.route === 'lookup') {\n    const vendors = (vendorResp.QueryResponse && vendorResp.QueryResponse.Vendor) || [];\n    const target = norm(inv.vendor_name);\n    const exact = vendors.filter((v) => v.Active !== false && (norm(v.DisplayName) === target || norm(v.CompanyName) === target));\n    if (exact.length === 1) vendor = exact[0];\n    else if (exact.length > 1) reasons.push('vendor_ambiguous_in_qbo');\n    else reasons.push('vendor_not_found_in_qbo: ' + inv.vendor_name);\n\n    const bills = (billResp.QueryResponse && billResp.QueryResponse.Bill) || [];\n    if (bills.some((b) => !vendor || (b.VendorRef && b.VendorRef.value === vendor.Id))) reasons.push('duplicate_invoice_number_already_in_qbo');\n\n    if (inv.total > Number(cfg.approvalThreshold)) reasons.push('over_approval_threshold_' + cfg.approvalThreshold);\n\n    if (!reasons.length) {\n      const account = { AccountRef: { value: String(cfg.expenseAccountId) } };\n      const lines = inv.line_items.length\n        ? inv.line_items.map((l) => ({ Amount: round2(l.amount), Description: String(l.description).slice(0, 4000) }))\n        : [{ Amount: round2(inv.subtotal), Description: 'Invoice ' + inv.invoice_number }];\n      if (inv.tax > 0) lines.push({ Amount: round2(inv.tax), Description: 'Tax' });\n      const lineTotal = round2(lines.reduce((s, l) => s + l.Amount, 0));\n      if (Math.abs(lineTotal - round2(inv.total)) > 0.01) reasons.push('bill_lines_do_not_sum_to_total');\n      qboBill = {\n        VendorRef: { value: vendor.Id },\n        TxnDate: inv.invoice_date,\n        DocNumber: inv.invoice_number,\n        PrivateNote: ('Created by n8n from email \"' + p.subject + '\" (' + p.attachmentName + ')').slice(0, 4000),\n        Line: lines.map((l) => ({ Amount: l.Amount, Description: l.Description, DetailType: 'AccountBasedExpenseLineDetail', AccountBasedExpenseLineDetail: account })),\n      };\n      if (/^\\d{4}-\\d{2}-\\d{2}$/.test(inv.due_date || '')) qboBill.DueDate = inv.due_date;\n    }\n  }\n\n  const route = reasons.length ? 'review' : 'post';\n  const summary = inv ? (inv.vendor_name + ' | invoice ' + inv.invoice_number + ' | ' + inv.total + ' ' + inv.currency) : p.attachmentName;\n  out.push({\n    json: {\n      ...p,\n      route,\n      reasons,\n      vendorId: vendor ? vendor.Id : '',\n      qboBill,\n      slackText: ':receipt: *Invoice needs review*\\n' + summary + '\\nReasons: ' + reasons.join(', ') + '\\nFrom: ' + p.from + '\\nSubject: ' + p.subject,\n    },\n    pairedItem: { item: i },\n  });\n}\nreturn out;\n"
      },
      "id": "d04b4546-2151-4612-bdec-a64329d7e1a7",
      "name": "Decide route",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        0
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "3a1bbd49-cf02-47f0-8ca0-95a925f60ba0",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "post",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "post"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "310c33da-f82e-4b26-9d3d-d3cc52679ca8",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "review",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "review"
            }
          ]
        },
        "options": {}
      },
      "id": "ce57d90d-bf8a-4789-97f2-3ca960a69667",
      "name": "Route",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1760,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').first().json.qboBaseUrl }}/v3/company/{{ $('Config').first().json.qboRealmId }}/bill",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "quickBooksOAuth2Api",
        "sendQuery": true,
        "specifyQuery": "keypair",
        "queryParameters": {
          "parameters": [
            {
              "name": "minorversion",
              "value": "75"
            }
          ]
        },
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.qboBill) }}",
        "options": {}
      },
      "id": "53640c5e-0cfc-457f-9960-6e7615e48ade",
      "name": "QBO: create bill",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1980,
        -120
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "markAsRead",
        "messageId": "={{ $('Decide route').item.json.messageId }}"
      },
      "id": "55b37591-aeaf-4961-9aad-a3c8cfc06152",
      "name": "Mark email processed",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2200,
        -120
      ]
    },
    {
      "parameters": {
        "authentication": "accessToken",
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#REPLACE_WITH_SLACK_CHANNEL"
        },
        "messageType": "text",
        "text": "={{ $json.slackText || (':warning: Invoice workflow error: ' + (($json.error && ($json.error.message || $json.error.description)) || JSON.stringify($json.error || $json).slice(0, 500)) + ' (execution ' + $execution.id + ')') }}",
        "otherOptions": {
          "includeLinkToWorkflow": true
        }
      },
      "id": "d2f874bf-e67f-4816-8959-449a1c784778",
      "name": "Post to review channel",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        2200,
        160
      ]
    }
  ],
  "connections": {
    "Gmail Trigger": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Build extraction request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build extraction request": {
      "main": [
        [
          {
            "node": "Claude: extract invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude: extract invoice": {
      "main": [
        [
          {
            "node": "Parse and validate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post to review channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse and validate": {
      "main": [
        [
          {
            "node": "QBO: find vendor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "QBO: find vendor": {
      "main": [
        [
          {
            "node": "QBO: find existing bill",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post to review channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "QBO: find existing bill": {
      "main": [
        [
          {
            "node": "Decide route",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post to review channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decide route": {
      "main": [
        [
          {
            "node": "Route",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route": {
      "main": [
        [
          {
            "node": "QBO: create bill",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post to review channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "QBO: create bill": {
      "main": [
        [
          {
            "node": "Mark email processed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post to review channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
