{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "AI Receipt Expense Memory",
  "tags": [],
  "nodes": [
    {
      "id": "48a8d1b3-12c5-4022-80d2-2669c80b7657",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -608,
        -368
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 896,
        "content": "## AI Receipt Expense Memory\n\n### How it works\n\nThis workflow watches Gmail for receipt emails, normalizes the email content, downloads receipt attachments, and checks configured expense memory stores to avoid processing duplicates. For new receipts, it uses OpenAI to extract expense details from email text first. If the email body has little/no usable text and a receipt image attachment is available, it falls back to OpenAI image OCR using the same JSON schema. It then optionally sends Slack and Microsoft Teams alerts, writes the expense record to Google Sheets and/or Airtable, and finishes with a final receipt-processing response.\n\n### Setup steps\n\n- Connect Gmail credentials and configure the Gmail trigger to match receipt emails or labels. Leave attachment download enabled for OCR fallback.\n- Edit the setup/configuration code node with receipt settings, duplicate-check options, OCR fallback settings, logging toggles, sheet/base identifiers, alert toggles, and webhook/channel details.\n- Connect OpenAI credentials and review the receipt extraction/OCR prompt and model settings.\n- If Slack alerts are enabled, connect Slack credentials and set the destination channel.\n- If Teams alerts are enabled, provide a valid Microsoft Teams incoming webhook URL.\n- If Google Sheets logging or lookup is enabled, connect Google Sheets credentials and configure the spreadsheet, sheet, and column mapping.\n- If Airtable logging or lookup is enabled, connect Airtable credentials and configure the base, table, and field mapping.\n\n### Customization\n\nAdjust the duplicate matching rules, OCR threshold, extracted receipt fields, alert message formatting, and which memory/log destinations are enabled in the configuration and code nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "f39f8ae0-28c5-4cc9-bdec-47ca45a342ad",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 320,
        "content": "## Gmail receipt intake\n\nStarts the workflow when a receipt-related email arrives in Gmail."
      },
      "typeVersion": 1
    },
    {
      "id": "7ec28c7a-82cc-47c4-bf0d-e1920db7c567",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Configure and normalize email\n\nLoads user-defined receipt-processing settings and converts the Gmail payload into a consistent structure for downstream checks."
      },
      "typeVersion": 1
    },
    {
      "id": "c9bb255d-ccc7-4beb-a1ed-a85fb39fa3a1",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        736,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 512,
        "content": "## Airtable memory lookup\n\nBranches based on whether Airtable memory is enabled, reads prior expense records when enabled, or builds a skip bundle when disabled."
      },
      "typeVersion": 1
    },
    {
      "id": "b5e4c55a-88d3-4295-a8c7-c048c73dd406",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1520,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 544,
        "content": "## Sheets memory lookup\n\nBranches based on whether Google Sheets memory is enabled, retrieves existing expense rows when enabled, or passes through the prior lookup bundle when disabled."
      },
      "typeVersion": 1
    },
    {
      "id": "56904c78-5fc8-422b-830c-92f4a08d13df",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2272,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Duplicate receipt gate\n\nCompares the normalized receipt against lookup rows and only allows new receipts to continue to extraction."
      },
      "typeVersion": 1
    },
    {
      "id": "c1d03c52-35e1-4c00-97be-bf9bb708baa3",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        624
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 304,
        "content": "## Extract receipt details\n\nUses OpenAI to parse receipt email text. If the body has little usable text and an image attachment is available, OpenAI image OCR extracts the same receipt JSON fields before analysis and alert formatting."
      },
      "typeVersion": 1
    },
    {
      "id": "44291956-dae1-4085-8f48-500eed1cc124",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2368,
        -368
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 496,
        "content": "## Slack receipt alert\n\nConditionally posts the prepared receipt alert to Slack, then records whether the Slack notification was sent or skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "64dbcde7-468c-446b-a2ea-6441b9446432",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3136,
        -32
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 496,
        "content": "## Teams receipt alert\n\nConditionally sends the receipt alert to Microsoft Teams through a webhook and records the sent or skipped status."
      },
      "typeVersion": 1
    },
    {
      "id": "f28cee7b-ae0a-448b-bf93-d30817060013",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3888,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 368,
        "content": "## Build log record\n\nCombines extracted receipt data and notification status into the expense record that will be written to enabled logs."
      },
      "typeVersion": 1
    },
    {
      "id": "50c0c7be-39a3-4950-8650-0fc0743fa2ce",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4160,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 320,
        "content": "## Sheets write decision\n\nChecks whether the final expense record should be written to Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "5a6f4ccd-da94-4d4a-8ddc-f273ad5ecafc",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4448,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 512,
        "content": "## Write Sheets expense\n\nAppends the expense to Google Sheets when enabled, confirms the write, or records that the Sheets write was skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "00e757fe-5917-41db-88c4-8ae016092106",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4944,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 320,
        "content": "## Airtable write decision\n\nChecks whether the final expense record should also be written to Airtable."
      },
      "typeVersion": 1
    },
    {
      "id": "9b75f06e-ebb5-4705-b3ec-3a283a3b27a8",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5216,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 512,
        "content": "## Write Airtable expense\n\nCreates the expense record in Airtable when enabled, confirms the write, or records that the Airtable write was skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "a7396889-f0f0-41c9-8c31-1609d5499ee6",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5712,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 368,
        "content": "## Finalize receipt result\n\nProduces the final response summarizing the receipt extraction, alert delivery, duplicate status, and logging outcomes."
      },
      "typeVersion": 1
    },
    {
      "id": "88888888-8888-8888-8888-000000000001",
      "name": "When Receipt Arrives in Gmail",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        0,
        320
      ],
      "parameters": {
        "filters": {
          "q": "label:Receipts newer_than:2d"
        },
        "options": {
          "downloadAttachments": true,
          "dataPropertyAttachmentsPrefixName": "attachment_"
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000002",
      "name": "Configure Receipt Settings",
      "type": "n8n-nodes-base.code",
      "position": [
        288,
        320
      ],
      "parameters": {
        "jsCode": "// ===== EDIT YOUR SETTINGS HERE =====\n// Defaults below match the Gumroad + n8n Creator listings.\n// Change only what you need; leave unused destinations as placeholders.\n\n// Where to log receipts:\n//   'sheets'   -> Google Sheets only\n//   'airtable' -> Airtable only\n//   'both'     -> Google Sheets and Airtable\nconst EXPENSE_LOG = 'sheets';\n\nconst GOOGLE_SHEET_ID = 'your-google-sheet-id';\nconst SHEETS_TAB_NAME = 'Expenses';\n\nconst AIRTABLE_BASE_ID = 'appXXXXXXXXXXXXXX';\nconst EXPENSES_TABLE_NAME = 'Expenses';\n\n// Where to send alerts for high-value, warranty, or review-needed receipts:\n//   'slack' -> Slack only\n//   'teams' -> Microsoft Teams only\n//   'both'  -> Slack and Teams\n//   'none'  -> no chat alerts\nconst NOTIFY_CHANNELS = 'slack';\nconst SLACK_CHANNEL = '#receipts';\nconst TEAMS_WEBHOOK_URL = 'https://outlook.office.com/webhook/your-teams-incoming-webhook';\n\nconst HIGH_VALUE_THRESHOLD = 250;\nconst DUPLICATE_WINDOW_DAYS = 45;\nconst WARRANTY_ALERT_ENABLED = true;\nconst ATTACHMENT_OCR_ENABLED = true;\nconst OCR_MIN_TEXT_CHARS = 80;\nconst DEFAULT_SOURCE = 'gmail_receipt';\n\nconst payload = $input.first().json;\nconst expenseLog = String(EXPENSE_LOG || 'sheets').toLowerCase().trim();\nconst notifyChannels = String(NOTIFY_CHANNELS || 'slack').toLowerCase().trim();\n\nreturn [\n  {\n    json: {\n      expenseLog,\n      useSheetsLog: expenseLog === 'sheets' || expenseLog === 'both',\n      useAirtableLog: expenseLog === 'airtable' || expenseLog === 'both',\n      googleSheetId: GOOGLE_SHEET_ID,\n      sheetsTabName: SHEETS_TAB_NAME,\n      airtableBaseId: AIRTABLE_BASE_ID,\n      expensesTableName: EXPENSES_TABLE_NAME,\n      notifyChannels,\n      useSlack: notifyChannels === 'slack' || notifyChannels === 'both',\n      useTeams: notifyChannels === 'teams' || notifyChannels === 'both',\n      slackChannel: SLACK_CHANNEL,\n      teamsWebhookUrl: TEAMS_WEBHOOK_URL,\n      highValueThreshold: HIGH_VALUE_THRESHOLD,\n      duplicateWindowDays: DUPLICATE_WINDOW_DAYS,\n      warrantyAlertEnabled: WARRANTY_ALERT_ENABLED,\n      attachmentOcrEnabled: ATTACHMENT_OCR_ENABLED,\n      ocrMinTextChars: OCR_MIN_TEXT_CHARS,\n      defaultSource: DEFAULT_SOURCE,\n      payload,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000003",
      "name": "Normalize Email Structure",
      "type": "n8n-nodes-base.code",
      "position": [
        560,
        320
      ],
      "parameters": {
        "jsCode": "const inputItem = $input.first();\nconst settings = inputItem.json;\nconst binary = inputItem.binary || {};\nconst raw = settings.payload || {};\n\nconst htmlToText = (value) => String(value || '')\n  .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n  .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n  .replace(/<br\\s*\\/?>/gi, '\\n')\n  .replace(/<\\/p>/gi, '\\n')\n  .replace(/<[^>]+>/g, ' ')\n  .replace(/&nbsp;/g, ' ')\n  .replace(/&amp;/g, '&')\n  .replace(/&lt;/g, '<')\n  .replace(/&gt;/g, '>')\n  .replace(/\\s+\\n/g, '\\n')\n  .replace(/[ \\t]+/g, ' ')\n  .trim();\n\nconst headers = raw.headers || raw.payload?.headers || {};\nconst subject = raw.subject || raw.Subject || headers.subject || raw.payload?.subject || '';\nconst fromEmail = raw.from || raw.From || headers.from || raw.payload?.from || '';\nconst messageId = raw.id || raw.messageId || raw.message_id || raw.payload?.id || `receipt-${Date.now()}`;\nconst receivedAt = raw.date || raw.internalDate || raw.receivedAt || new Date().toISOString();\nconst textBody = raw.textPlain || raw.text || raw.snippet || raw.body?.text || '';\nconst htmlBody = raw.textHtml || raw.html || raw.body?.html || '';\nconst rawText = htmlToText(textBody || htmlBody || raw.snippet || JSON.stringify(raw).slice(0, 6000));\n\nconst compact = rawText.toLowerCase().replace(/\\s+/g, ' ').slice(0, 1000);\nconst receiptHash = `${fromEmail}|${subject}|${compact}`.toLowerCase();\nconst attachmentBinaryProperties = Object.keys(binary).filter((name) => name.startsWith('attachment_'));\nconst imageAttachmentProperties = attachmentBinaryProperties.filter((name) =>\n  String(binary[name]?.mimeType || '').toLowerCase().startsWith('image/')\n);\nconst pdfAttachmentProperties = attachmentBinaryProperties.filter((name) =>\n  String(binary[name]?.mimeType || '').toLowerCase().includes('pdf')\n);\nconst ocrBinaryProperty = imageAttachmentProperties[0] || '';\nconst needsOcrFallback = Boolean(\n  settings.attachmentOcrEnabled &&\n    rawText.length < (settings.ocrMinTextChars ?? 80) &&\n    ocrBinaryProperty\n);\n\nreturn [\n  {\n    json: {\n      ...settings,\n      messageId: String(messageId),\n      receivedAt,\n      source: raw.source || settings.defaultSource || 'gmail_receipt',\n      originalSubject: String(subject),\n      fromEmail: String(fromEmail),\n      rawText: rawText.slice(0, 10000),\n      rawTextSnippet: rawText.slice(0, 1000),\n      receiptHash,\n      attachmentBinaryProperties,\n      imageAttachmentProperties,\n      pdfAttachmentProperties,\n      ocrBinaryProperty,\n      needsOcrFallback,\n      _mockExistingReceipts: Array.isArray(raw._mockExistingReceipts) ? raw._mockExistingReceipts : [],\n      _testScenario: raw._testScenario || raw.scenario || '',\n    },\n    binary,\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000040",
      "name": "Check Airtable Log Usage",
      "type": "n8n-nodes-base.if",
      "position": [
        780,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useAirtableLog }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000004",
      "name": "Fetch Airtable Expenses",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1040,
        200
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.airtableBaseId }}"
        },
        "limit": 10,
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.expensesTableName }}"
        },
        "options": {},
        "operation": "search",
        "returnAll": false,
        "filterByFormula": "=OR({Message ID} = '{{ $json.messageId }}', {Receipt Hash} = '{{ $json.receiptHash }}')"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1,
      "alwaysOutputData": true
    },
    {
      "id": "88888888-8888-8888-8888-000000000041",
      "name": "Compile Airtable Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1300,
        200
      ],
      "parameters": {
        "jsCode": "const source = $('Normalize Email Structure').first();\nconst receipt = source.json;\nconst rows = $input\n  .all()\n  .map((item) => item.json)\n  .filter((row) => !row._mockEmpty)\n  .map((row) => row.fields || row);\nreturn [{ json: { ...receipt, _lookupRows: rows }, binary: source.binary || {} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000042",
      "name": "Bypass Airtable Data Collection",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        420
      ],
      "parameters": {
        "jsCode": "const source = $('Normalize Email Structure').first();\nconst receipt = source.json;\nreturn [{ json: { ...receipt, _lookupRows: [] }, binary: source.binary || {} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000043",
      "name": "Check Sheets Log Usage",
      "type": "n8n-nodes-base.if",
      "position": [
        1560,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useSheetsLog }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000044",
      "name": "Fetch Sheets Expenses",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1820,
        200
      ],
      "parameters": {
        "options": {},
        "resource": "sheet",
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.sheetsTabName }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.googleSheetId }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5,
      "alwaysOutputData": true
    },
    {
      "id": "88888888-8888-8888-8888-000000000045",
      "name": "Combine Sheets Data",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        200
      ],
      "parameters": {
        "jsCode": "function fromNode(name) {\n  try {\n    return $(name).first().json;\n  } catch (error) {\n    return null;\n  }\n}\n\nconst prev =\n  fromNode('Compile Airtable Data') ||\n  fromNode('Bypass Airtable Data Collection') ||\n  $('Normalize Email Structure').first().json;\nconst source = $('Normalize Email Structure').first();\n\nconst sheetRows = $input\n  .all()\n  .map((item) => item.json)\n  .filter((row) => !row._mockEmpty)\n  .map((row) => row.fields || row);\n\nreturn [{ json: { ...prev, _lookupRows: [...(prev._lookupRows || []), ...sheetRows] }, binary: source.binary || {} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000046",
      "name": "Transfer Lookup Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1820,
        420
      ],
      "parameters": {
        "jsCode": "const inputItem = $input.first();\nreturn [{ json: inputItem.json, binary: inputItem.binary || {} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000005",
      "name": "Check Receipt Duplication",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        320
      ],
      "parameters": {
        "jsCode": "const inputItem = $input.first();\nconst receipt = inputItem.json;\nconst rows = Array.isArray(receipt._lookupRows) ? receipt._lookupRows : [];\nconst windowDays = receipt.duplicateWindowDays ?? 45;\nconst cutoff = Date.now() - windowDays * 24 * 60 * 60 * 1000;\n\nconst isDuplicate = rows.some((row) => {\n  const record = row.fields || row;\n  if (record._mockEmpty) return false;\n  const existingMessageId = String(record['Message ID'] || record.messageId || '').trim();\n  const existingHash = String(record['Receipt Hash'] || record.receiptHash || '').toLowerCase();\n  const existingAt = Date.parse(record['Received At'] || record.receivedAt || record.createdTime || '');\n\n  if (receipt.messageId && existingMessageId && receipt.messageId === existingMessageId) {\n    if (!existingAt || existingAt >= cutoff) return true;\n  }\n\n  if (receipt.receiptHash && existingHash && receipt.receiptHash === existingHash) {\n    if (!existingAt || existingAt >= cutoff) return true;\n  }\n\n  return false;\n});\n\nreturn [{ json: { ...receipt, isDuplicate }, binary: inputItem.binary || {} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000006",
      "name": "Determine New Receipt Status",
      "type": "n8n-nodes-base.if",
      "position": [
        2580,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.isDuplicate }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000070",
      "name": "Check Attachment OCR Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        2840,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.needsOcrFallback }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000071",
      "name": "AI Receipt Attachment OCR",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1648,
        1008
      ],
      "parameters": {
        "text": "=You are extracting expense data from a receipt image attachment because the email body did not contain enough readable text. Return ONLY valid JSON with keys: isReceipt (boolean), extractionStatus (extracted|not_receipt|failed), failureReason (string), merchant, purchaseDate, total, currency, category, taxDeductible, taxCategory, paymentMethod, lastFour, items, warrantyItems, returnDeadline, warrantyEndDate, aiSummary, confidence, needsReview, notes. If the image is not a receipt/invoice, set isReceipt=false and extractionStatus=not_receipt. Use ISO dates when possible. confidence is 0-100. needsReview true when totals conflict, merchant is unclear, or total is missing.\n\nEmail context:\nFrom: {{ $json.fromEmail }}\nSubject: {{ $json.originalSubject }}\nReceived: {{ $json.receivedAt }}",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {
          "detail": "high",
          "maxTokens": 900
        },
        "resource": "image",
        "simplify": true,
        "inputType": "base64",
        "operation": "analyze",
        "binaryPropertyName": "={{ $json.ocrBinaryProperty || 'attachment_0' }}"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8,
      "continueOnFail": true
    },
    {
      "id": "88888888-8888-8888-8888-000000000007",
      "name": "AI Receipt Data Extraction",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1648,
        752
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {
          "temperature": 0.1
        },
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "You extract receipt and invoice data for expense tracking. Return ONLY valid JSON with keys: isReceipt (boolean), extractionStatus (extracted|not_receipt|failed), failureReason (string), merchant, purchaseDate, total, currency, category, taxDeductible, taxCategory, paymentMethod, lastFour, items, warrantyItems, returnDeadline, warrantyEndDate, aiSummary, confidence, needsReview, notes. If the email is not a receipt/invoice/purchase confirmation, set isReceipt=false and extractionStatus=not_receipt. Use ISO dates when possible. confidence is 0-100. needsReview true when totals conflict, merchant is unclear, or total is missing."
            },
            {
              "role": "user",
              "content": "=Receipt email:\nFrom: {{ $('Check Receipt Duplication').item.json.fromEmail }}\nSubject: {{ $('Check Receipt Duplication').item.json.originalSubject }}\nReceived: {{ $('Check Receipt Duplication').item.json.receivedAt }}\nBody:\n{{ $('Check Receipt Duplication').item.json.rawText }}"
            }
          ]
        },
        "resource": "text",
        "operation": "message"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8,
      "continueOnFail": true
    },
    {
      "id": "88888888-8888-8888-8888-000000000008",
      "name": "Examine Extracted Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1904,
        752
      ],
      "parameters": {
        "jsCode": "const receiptItem = $('Check Receipt Duplication').first();\nconst receipt = receiptItem.json;\nconst aiItem = $input.first().json;\nconst raw =\n  aiItem.message?.content ||\n  aiItem.content ||\n  aiItem.text ||\n  aiItem.output ||\n  aiItem.output_text ||\n  aiItem.choices?.[0]?.message?.content ||\n  '';\n\nlet extraction = {\n  isReceipt: true,\n  extractionStatus: 'extracted',\n  failureReason: '',\n  merchant: 'Unknown merchant',\n  purchaseDate: '',\n  total: 0,\n  currency: 'USD',\n  category: 'Uncategorized',\n  taxDeductible: false,\n  taxCategory: '',\n  paymentMethod: '',\n  lastFour: '',\n  items: [],\n  warrantyItems: [],\n  returnDeadline: '',\n  warrantyEndDate: '',\n  aiSummary: '',\n  confidence: 50,\n  needsReview: true,\n  notes: '',\n};\n\nlet parsedOk = false;\ntry {\n  const jsonMatch = String(raw).match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) {\n    extraction = { ...extraction, ...JSON.parse(jsonMatch[0]) };\n    parsedOk = true;\n  }\n} catch (error) {\n  extraction.aiSummary = String(raw).slice(0, 500);\n  extraction.extractionStatus = 'failed';\n  extraction.isReceipt = false;\n  extraction.failureReason = 'OpenAI returned text that was not valid receipt JSON';\n}\n\nif (!parsedOk && !extraction.failureReason) {\n  extraction.extractionStatus = 'failed';\n  extraction.failureReason = 'OpenAI did not return a JSON object';\n}\n\nconst toList = (value) => {\n  if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);\n  return String(value || '')\n    .split(/\\n|, /)\n    .map((item) => item.trim())\n    .filter(Boolean);\n};\n\nconst total = Number(String(extraction.total || 0).replace(/[^0-9.-]/g, '')) || 0;\nconst confidence = Math.max(0, Math.min(100, Number(extraction.confidence) || 50));\nconst items = toList(extraction.items);\nconst warrantyItems = toList(extraction.warrantyItems);\nconst highValue = total >= (receipt.highValueThreshold ?? 250);\nconst warrantyRelevant = Boolean(\n  receipt.warrantyAlertEnabled &&\n    (warrantyItems.length || extraction.returnDeadline || extraction.warrantyEndDate)\n);\nconst needsReview = Boolean(\n  extraction.needsReview || confidence < 70 || total <= 0 || !extraction.merchant || extraction.merchant === 'Unknown merchant'\n);\nconst isReceipt = extraction.isReceipt !== false && extraction.extractionStatus !== 'not_receipt';\nconst severeMissingFields = Boolean(total <= 0 && (!extraction.merchant || extraction.merchant === 'Unknown merchant'));\nconst pdfOnlyNeedsExtraction = Boolean(\n  (receipt.pdfAttachmentProperties || []).length &&\n    !receipt.needsOcrFallback &&\n    String(receipt.rawText || '').length < (receipt.ocrMinTextChars ?? 80)\n);\nconst extractionFailed = Boolean(\n  aiItem.error ||\n    extraction.extractionStatus === 'failed' ||\n    !parsedOk ||\n    (pdfOnlyNeedsExtraction && severeMissingFields)\n);\nconst extractionSucceeded = Boolean(isReceipt && !extractionFailed);\n\nlet route = 'standard';\nif (!isReceipt) route = 'not_receipt';\nelse if (extractionFailed) route = 'extraction_failed';\nelse if (needsReview) route = 'review';\nelse if (highValue) route = 'high_value';\nelse if (warrantyRelevant) route = 'warranty';\n\nconst shouldAlert = receipt.notifyChannels !== 'none' && route !== 'standard' && extractionSucceeded;\n\nreturn [\n  {\n    json: {\n      ...receipt,\n      merchant: String(extraction.merchant || 'Unknown merchant'),\n      purchaseDate: String(extraction.purchaseDate || ''),\n      total,\n      currency: String(extraction.currency || 'USD'),\n      category: String(extraction.category || 'Uncategorized'),\n      taxDeductible: Boolean(extraction.taxDeductible),\n      taxCategory: String(extraction.taxCategory || ''),\n      paymentMethod: String(extraction.paymentMethod || ''),\n      lastFour: String(extraction.lastFour || ''),\n      items: items.join(' | '),\n      warrantyItems: warrantyItems.join(' | '),\n      returnDeadline: String(extraction.returnDeadline || ''),\n      warrantyEndDate: String(extraction.warrantyEndDate || ''),\n      aiSummary: String(extraction.aiSummary || `${extraction.merchant || 'Receipt'} - ${total}`),\n      extractionSource: receipt.needsOcrFallback ? `attachment_ocr:${receipt.ocrBinaryProperty}` : 'email_text',\n      isReceipt,\n      extractionStatus: extractionFailed ? 'failed' : isReceipt ? 'extracted' : 'not_receipt',\n      extractionSucceeded,\n      extractionFailureReason:\n        aiItem.error?.message ||\n        extraction.failureReason ||\n        (pdfOnlyNeedsExtraction && severeMissingFields\n          ? 'PDF-only receipt attachment could not be extracted from the email body'\n          : '') ||\n        (!isReceipt ? 'Email or attachment does not appear to be a receipt' : ''),\n      confidence,\n      needsReview,\n      highValue,\n      warrantyRelevant,\n      shouldAlert,\n      route,\n      notes: String(extraction.notes || ''),\n    },\n    binary: receiptItem.binary || {},\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000072",
      "name": "Check Receipt Extraction Result",
      "type": "n8n-nodes-base.if",
      "position": [
        2160,
        752
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.extractionSucceeded }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000073",
      "name": "Complete Skipped Receipt Processing",
      "type": "n8n-nodes-base.code",
      "position": [
        2160,
        1008
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nconst reason = item.isReceipt === false ? 'non_receipt' : 'extraction_failed';\n\nreturn [\n  {\n    json: {\n      status: 'skipped',\n      reason,\n      message:\n        reason === 'non_receipt'\n          ? 'Email was processed successfully but does not appear to be a receipt.'\n          : 'Receipt extraction failed. Check the email text, attachment quality, or OpenAI node output.',\n      messageId: item.messageId,\n      receiptHash: item.receiptHash,\n      extractionStatus: item.extractionStatus,\n      extractionFailureReason: item.extractionFailureReason || '',\n      extractionSource: item.extractionSource || '',\n      originalSubject: item.originalSubject,\n      fromEmail: item.fromEmail,\n      testScenario: item._testScenario || '',\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000009",
      "name": "Format Receipt Alert",
      "type": "n8n-nodes-base.code",
      "position": [
        2160,
        752
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nconst money = `${item.currency || 'USD'} ${Number(item.total || 0).toFixed(2)}`;\nconst reason =\n  item.route === 'review'\n    ? 'Needs review'\n    : item.route === 'high_value'\n      ? 'High-value receipt'\n      : item.route === 'warranty'\n        ? 'Warranty or return date found'\n        : 'Receipt logged';\n\nconst details = [\n  `Reason: ${reason}`,\n  `Merchant: ${item.merchant}`,\n  `Total: ${money}`,\n  `Category: ${item.category}`,\n  `Purchase date: ${item.purchaseDate || 'Not found'}`,\n  `Tax deductible: ${item.taxDeductible ? 'yes' : 'no'}`,\n  `Tax category: ${item.taxCategory || 'None'}`,\n  `Confidence: ${item.confidence}/100`,\n  item.warrantyItems ? `Warranty items: ${item.warrantyItems}` : '',\n  item.returnDeadline ? `Return deadline: ${item.returnDeadline}` : '',\n  item.warrantyEndDate ? `Warranty end: ${item.warrantyEndDate}` : '',\n  `Subject: ${item.originalSubject}`,\n].filter(Boolean);\n\nconst slackMessage = [`*${reason}: ${item.merchant}*`, '', ...details.map((line) => `\u2022 ${line}`)].join('\\n');\nconst teamsMessage = [`**${reason}: ${item.merchant}**`, '', ...details.map((line) => `- ${line}`)].join('\\n');\n\nreturn [\n  {\n    json: {\n      ...item,\n      useSlackAlert: Boolean(item.shouldAlert && item.useSlack),\n      useTeamsAlert: Boolean(item.shouldAlert && item.useTeams),\n      slackMessage,\n      teamsMessage,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000050",
      "name": "Check Slack Alert Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        2416,
        -112
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useSlackAlert }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000051",
      "name": "Send Slack Receipt Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        2688,
        -240
      ],
      "parameters": {
        "text": "={{ $json.slackMessage }}",
        "select": "channel",
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.slackChannel }}"
        },
        "operation": "post",
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000052",
      "name": "Validate Slack Alert Sent",
      "type": "n8n-nodes-base.code",
      "position": [
        2944,
        -240
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, alertSent: 'yes' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000053",
      "name": "Omit Slack Alert",
      "type": "n8n-nodes-base.code",
      "position": [
        2688,
        -32
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, alertSent: item.alertSent || 'no' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000054",
      "name": "Check Teams Alert Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        3180,
        220
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useTeamsAlert }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000055",
      "name": "Send Teams Receipt Alert",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3440,
        100
      ],
      "parameters": {
        "url": "={{ $json.teamsWebhookUrl }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ text: $json.teamsMessage }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000056",
      "name": "Validate Teams Alert Sent",
      "type": "n8n-nodes-base.code",
      "position": [
        3700,
        100
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, alertSent: 'yes' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000057",
      "name": "Omit Teams Alert",
      "type": "n8n-nodes-base.code",
      "position": [
        3440,
        300
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, alertSent: item.alertSent || 'no' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000010",
      "name": "Create Expense Log",
      "type": "n8n-nodes-base.code",
      "position": [
        3940,
        220
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [\n  {\n    json: {\n      expenseLog: item.expenseLog,\n      useSheetsLog: item.useSheetsLog,\n      useAirtableLog: item.useAirtableLog,\n      googleSheetId: item.googleSheetId,\n      sheetsTabName: item.sheetsTabName,\n      airtableBaseId: item.airtableBaseId,\n      expensesTableName: item.expensesTableName,\n      'Received At': item.receivedAt,\n      Source: item.source,\n      'Message ID': item.messageId,\n      'Receipt Hash': item.receiptHash,\n      Merchant: item.merchant,\n      'Purchase Date': item.purchaseDate,\n      Total: item.total,\n      Currency: item.currency,\n      Category: item.category,\n      'Tax Deductible': item.taxDeductible ? 'yes' : 'no',\n      'Tax Category': item.taxCategory,\n      'Payment Method': item.paymentMethod,\n      'Last Four': item.lastFour,\n      Items: item.items,\n      'Warranty Items': item.warrantyItems,\n      'Return Deadline': item.returnDeadline,\n      'Warranty End Date': item.warrantyEndDate,\n      'AI Summary': item.aiSummary,\n      'Extraction Source': item.extractionSource,\n      'Is Receipt': item.isReceipt ? 'yes' : 'no',\n      'Extraction Status': item.extractionStatus,\n      'Extraction Failure Reason': item.extractionFailureReason,\n      Confidence: item.confidence,\n      'Needs Review': item.needsReview ? 'yes' : 'no',\n      'High Value': item.highValue ? 'yes' : 'no',\n      'Alert Sent': item.alertSent || 'no',\n      Status:\n        item.route === 'review'\n          ? 'Needs Review'\n          : item.route === 'high_value'\n            ? 'High Value'\n            : item.route === 'warranty'\n              ? 'Warranty / Return'\n              : 'Logged',\n      'Original Subject': item.originalSubject,\n      'From Email': item.fromEmail,\n      Notes: item.notes,\n      'Raw Text Snippet': item.rawTextSnippet,\n      _testScenario: item._testScenario || '',\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000060",
      "name": "Check Sheets Log Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        4200,
        220
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useSheetsLog }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000061",
      "name": "Log Expense in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4496,
        96
      ],
      "parameters": {
        "columns": {
          "value": {
            "Items": "={ $json[\"Items\"] }",
            "Notes": "={ $json[\"Notes\"] }",
            "Total": "={ $json[\"Total\"] }",
            "Source": "={ $json[\"Source\"] }",
            "Status": "={ $json[\"Status\"] }",
            "Category": "={ $json[\"Category\"] }",
            "Currency": "={ $json[\"Currency\"] }",
            "Merchant": "={ $json[\"Merchant\"] }",
            "Last Four": "={ $json[\"Last Four\"] }",
            "AI Summary": "={ $json[\"AI Summary\"] }",
            "Alert Sent": "={ $json[\"Alert Sent\"] }",
            "Confidence": "={ $json[\"Confidence\"] }",
            "From Email": "={ $json[\"From Email\"] }",
            "High Value": "={ $json[\"High Value\"] }",
            "Is Receipt": "={ $json[\"Is Receipt\"] }",
            "Message ID": "={ $json[\"Message ID\"] }",
            "Received At": "={ $json[\"Received At\"] }",
            "Needs Review": "={ $json[\"Needs Review\"] }",
            "Receipt Hash": "={ $json[\"Receipt Hash\"] }",
            "Tax Category": "={ $json[\"Tax Category\"] }",
            "Purchase Date": "={ $json[\"Purchase Date\"] }",
            "Payment Method": "={ $json[\"Payment Method\"] }",
            "Tax Deductible": "={ $json[\"Tax Deductible\"] }",
            "Warranty Items": "={ $json[\"Warranty Items\"] }",
            "Return Deadline": "={ $json[\"Return Deadline\"] }",
            "Original Subject": "={ $json[\"Original Subject\"] }",
            "Raw Text Snippet": "={ $json[\"Raw Text Snippet\"] }",
            "Extraction Source": "={ $json[\"Extraction Source\"] }",
            "Extraction Status": "={ $json[\"Extraction Status\"] }",
            "Warranty End Date": "={ $json[\"Warranty End Date\"] }",
            "Extraction Failure Reason": "={ $json[\"Extraction Failure Reason\"] }"
          },
          "schema": [
            {
              "id": "Received At",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Received At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Source",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Message ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Message ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Receipt Hash",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Receipt Hash",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Merchant",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Merchant",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Purchase Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Purchase Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Currency",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Currency",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tax Deductible",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Tax Deductible",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tax Category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Tax Category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Payment Method",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Payment Method",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Last Four",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Last Four",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Items",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Items",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Warranty Items",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Warranty Items",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Return Deadline",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Return Deadline",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Warranty End Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Warranty End Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "AI Summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "AI Summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Extraction Source",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Extraction Source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Is Receipt",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Is Receipt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Extraction Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Extraction Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Extraction Failure Reason",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Extraction Failure Reason",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Confidence",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Confidence",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Needs Review",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Needs Review",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "High Value",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "High Value",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Alert Sent",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Alert Sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Original Subject",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Original Subject",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "From Email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "From Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Notes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Raw Text Snippet",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Raw Text Snippet",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "resource": "sheet",
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.sheetsTabName }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.googleSheetId }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "88888888-8888-8888-8888-000000000062",
      "name": "Verify Sheets Log Entry",
      "type": "n8n-nodes-base.code",
      "position": [
        4752,
        96
      ],
      "parameters": {
        "jsCode": "const prior = $('Create Expense Log').first().json;\nreturn [{ json: { ...prior, sheetsLogged: 'yes' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000063",
      "name": "Bypass Sheets Logging",
      "type": "n8n-nodes-base.code",
      "position": [
        4496,
        304
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, sheetsLogged: 'no' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000064",
      "name": "Check Airtable Log Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        4992,
        224
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "+1234567890",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useAirtableLog }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "88888888-8888-8888-8888-000000000065",
      "name": "Log Expense in Airtable",
      "type": "n8n-nodes-base.airtable",
      "position": [
        5264,
        96
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.airtableBaseId }}"
        },
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.expensesTableName }}"
        },
        "columns": {
          "value": {
            "Items": "={ $json[\"Items\"] }",
            "Notes": "={ $json[\"Notes\"] }",
            "Total": "={ $json[\"Total\"] }",
            "Source": "={ $json[\"Source\"] }",
            "Status": "={ $json[\"Status\"] }",
            "Category": "={ $json[\"Category\"] }",
            "Currency": "={ $json[\"Currency\"] }",
            "Merchant": "={ $json[\"Merchant\"] }",
            "Last Four": "={ $json[\"Last Four\"] }",
            "AI Summary": "={ $json[\"AI Summary\"] }",
            "Alert Sent": "={ $json[\"Alert Sent\"] }",
            "Confidence": "={ $json[\"Confidence\"] }",
            "From Email": "={ $json[\"From Email\"] }",
            "High Value": "={ $json[\"High Value\"] }",
            "Is Receipt": "={ $json[\"Is Receipt\"] }",
            "Message ID": "={ $json[\"Message ID\"] }",
            "Received At": "={ $json[\"Received At\"] }",
            "Needs Review": "={ $json[\"Needs Review\"] }",
            "Receipt Hash": "={ $json[\"Receipt Hash\"] }",
            "Tax Category": "={ $json[\"Tax Category\"] }",
            "Purchase Date": "={ $json[\"Purchase Date\"] }",
            "Payment Method": "={ $json[\"Payment Method\"] }",
            "Tax Deductible": "={ $json[\"Tax Deductible\"] }",
            "Warranty Items": "={ $json[\"Warranty Items\"] }",
            "Return Deadline": "={ $json[\"Return Deadline\"] }",
            "Original Subject": "={ $json[\"Original Subject\"] }",
            "Raw Text Snippet": "={ $json[\"Raw Text Snippet\"] }",
            "Extraction Source": "={ $json[\"Extraction Source\"] }",
            "Extraction Status": "={ $json[\"Extraction Status\"] }",
            "Warranty End Date": "={ $json[\"Warranty End Date\"] }",
            "Extraction Failure Reason": "={ $json[\"Extraction Failure Reason\"] }"
          },
          "schema": [
            {
              "id": "Received At",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Received At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Source",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Message ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Message ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Receipt Hash",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Receipt Hash",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Merchant",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Merchant",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Purchase Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Purchase Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Currency",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Currency",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tax Deductible",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Tax Deductible",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tax Category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Tax Category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Payment Method",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Payment Method",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Last Four",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Last Four",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Items",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Items",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Warranty Items",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Warranty Items",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Return Deadline",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Return Deadline",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Warranty End Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Warranty End Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "AI Summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "AI Summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Extraction Source",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Extraction Source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Is Receipt",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Is Receipt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Extraction Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Extraction Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Extraction Failure Reason",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Extraction Failure Reason",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Confidence",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Confidence",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Needs Review",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Needs Review",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "High Value",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "High Value",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Alert Sent",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Alert Sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Original Subject",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Original Subject",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "From Email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "From Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Notes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Raw Text Snippet",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Raw Text Snippet",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "88888888-8888-8888-8888-000000000066",
      "name": "Verify Airtable Log Entry",
      "type": "n8n-nodes-base.code",
      "position": [
        5536,
        96
      ],
      "parameters": {
        "jsCode": "const prior = $('Check Airtable Log Requirement').first().json;\nreturn [{ json: { ...prior, airtableLogged: 'yes' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000067",
      "name": "Bypass Airtable Logging",
      "type": "n8n-nodes-base.code",
      "position": [
        5264,
        304
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, airtableLogged: 'no' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000011",
      "name": "Complete Receipt Processing",
      "type": "n8n-nodes-base.code",
      "position": [
        5760,
        224
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [\n  {\n    json: {\n      status: 'ok',\n      expenseLog: item.expenseLog,\n      sheetsLogged: item.sheetsLogged || 'no',\n      airtableLogged: item.airtableLogged || 'no',\n      merchant: item.Merchant,\n      total: item.Total,\n      category: item.Category,\n      needsReview: item['Needs Review'],\n      highValue: item['High Value'],\n      alertSent: item['Alert Sent'],\n      testScenario: item._testScenario || '',\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "88888888-8888-8888-8888-000000000099",
      "name": "Complete Duplicate Receipt Processing",
      "type": "n8n-nodes-base.code",
      "position": [
        2840,
        520
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [\n  {\n    json: {\n      status: 'skipped',\n      reason: item.isDuplicate ? 'duplicate_receipt' : 'filtered',\n      messageId: item.messageId,\n      receiptHash: item.receiptHash,\n      testScenario: item._testScenario || '',\n    },\n  },\n];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "ai-receipt-expense-memory-v1",
  "connections": {
    "Omit Slack Alert": {
      "main": [
        [
          {
            "node": "Check Teams Alert Requirement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Omit Teams Alert": {
      "main": [
        [
          {
            "node": "Create Expense Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Expense Log": {
      "main": [
        [
          {
            "node": "Check Sheets Log Requirement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Sheets Data": {
      "main": [
        [
          {
            "node": "Check Receipt Duplication",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Receipt Alert": {
      "main": [
        [
          {
            "node": "Check Slack Alert Requirement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transfer Lookup Data": {
      "main": [
        [
          {
            "node": "Check Receipt Duplication",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bypass Sheets Logging": {
      "main": [
        [
          {
            "node": "Check Airtable Log Requirement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compile Airtable Data": {
      "main": [
        [
          {
            "node": "Check Sheets Log Usage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Sheets Expenses": {
      "main": [
        [
          {
            "node": "Combine Sheets Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Expense in Sheets": {
      "main": [
        [
          {
            "node": "Verify Sheets Log Entry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Sheets Log Usage": {
      "main": [
        [
          {
            "node": "Fetch Sheets Expenses",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Transfer Lookup Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Examine Extracted Data": {
      "main": [
        [
          {
            "node": "Check Receipt Extraction Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bypass Airtable Logging": {
      "main": [
        [
          {
            "node": "Complete Receipt Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Airtable Expenses": {
      "main": [
        [
          {
            "node": "Compile Airtable Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Expense in Airtable": {
      "main": [
        [
          {
            "node": "Verify Airtable Log Entry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Sheets Log Entry": {
      "main": [
        [
          {
            "node": "Check Airtable Log Requirement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Airtable Log Usage": {
      "main": [
        [
          {
            "node": "Fetch Airtable Expenses",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Bypass Airtable Data Collection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Slack Receipt Alert": {
      "main": [
        [
          {
            "node": "Validate Slack Alert Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Teams Receipt Alert": {
      "main": [
        [
          {
            "node": "Validate Teams Alert Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Receipt Attachment OCR": {
      "main": [
        [
          {
            "node": "Examine Extracted Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Receipt Duplication": {
      "main": [
        [
          {
            "node": "Determine New Receipt Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Email Structure": {
      "main": [
        [
          {
            "node": "Check Airtable Log Usage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Slack Alert Sent": {
      "main": [
        [
          {
            "node": "Check Teams Alert Requirement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Teams Alert Sent": {
      "main": [
        [
          {
            "node": "Create Expense Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Airtable Log Entry": {
      "main": [
        [
          {
            "node": "Complete Receipt Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Receipt Data Extraction": {
      "main": [
        [
          {
            "node": "Examine Extracted Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Receipt Settings": {
      "main": [
        [
          {
            "node": "Normalize Email Structure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Sheets Log Requirement": {
      "main": [
        [
          {
            "node": "Log Expense in Sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Bypass Sheets Logging",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Determine New Receipt Status": {
      "main": [
        [
          {
            "node": "Check Attachment OCR Requirement",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Complete Duplicate Receipt Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Slack Alert Requirement": {
      "main": [
        [
          {
            "node": "Send Slack Receipt Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Omit Slack Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Teams Alert Requirement": {
      "main": [
        [
          {
            "node": "Send Teams Receipt Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Omit Teams Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Receipt Arrives in Gmail": {
      "main": [
        [
          {
            "node": "Configure Receipt Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Airtable Log Requirement": {
      "main": [
        [
          {
            "node": "Log Expense in Airtable",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Bypass Airtable Logging",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bypass Airtable Data Collection": {
      "main": [
        [
          {
            "node": "Check Sheets Log Usage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Receipt Extraction Result": {
      "main": [
        [
          {
            "node": "Format Receipt Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Complete Skipped Receipt Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Attachment OCR Requirement": {
      "main": [
        [
          {
            "node": "AI Receipt Attachment OCR",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "AI Receipt Data Extraction",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}