{
  "id": "ilmh26XJsIWqQESo",
  "name": "Extract receipts and invoices from Gmail with AI and log them to Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "overview",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1264,
        48
      ],
      "parameters": {
        "width": 696,
        "height": 752,
        "content": "## Receipt & invoice tracker \u2192 Google Sheets\n\nFind receipt and invoice emails, let AI pull out the vendor, amount, date, category and invoice number, and append each one as a row in a Google Sheet, so your expense log builds itself.\n\n### How it works\nOn a schedule it searches Gmail for receipt-like emails in your chosen window. A Code node flattens each message to plain text, then a single AI call reads them all and returns structured fields per email, including whether it is really a receipt (so marketing emails are dropped), the total amount, currency, purchase date, a category from your list, and any invoice or order number. The workflow turns the confirmed receipts into rows and appends them to your expense sheet.\n\n### Setup\n- Edit the config node: your Gmail search query, default currency and category list.\n- Add credentials in the canvas: Gmail, Anthropic on the extract HTTP node, and Google Sheets pointed at your expense sheet.\n- The Schedule Trigger runs daily; match the look-back window to the schedule.\n\n### Customization\nSwap Google Sheets for Airtable, Notion or a database; add attachment parsing for PDF invoices; or branch high-value expenses to Slack for approval.\n\nBuilt by **nocode.expert** \u2014 done-for-you automation and AI workflows. https://nocode.expert"
      },
      "typeVersion": 1
    },
    {
      "id": "section-find-receipts",
      "name": "Section: Find receipts",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        64,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 508,
        "height": 480,
        "content": "## 1. Find receipt emails\nSearch Gmail for invoices and receipts in your window, then flatten each to plain text."
      },
      "typeVersion": 1
    },
    {
      "id": "section-extract-fields",
      "name": "Section: Extract fields",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        672,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 480,
        "content": "## 2. Extract with AI\nOne AI call returns vendor, amount, currency, date, category and invoice number per email, and drops non-receipts."
      },
      "typeVersion": 1
    },
    {
      "id": "section-log-to-sheet",
      "name": "Section: Log to Sheet",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 872,
        "height": 480,
        "content": "## 3. Log to Sheets\nConfirmed receipts become rows appended to your expense sheet. Swap for Airtable or Notion."
      },
      "typeVersion": 1
    },
    {
      "id": "every-day",
      "name": "Every day",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -128,
        480
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "set-config-search-categories",
      "name": "Set config: search & categories",
      "type": "n8n-nodes-base.set",
      "position": [
        208,
        480
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "gmailQuery",
              "type": "string",
              "value": "(invoice OR receipt OR \"order confirmation\") newer_than:2d"
            },
            {
              "id": "a2",
              "name": "defaultCurrency",
              "type": "string",
              "value": "USD"
            },
            {
              "id": "a3",
              "name": "categories",
              "type": "string",
              "value": "Software, Hosting, Advertising, Travel, Meals, Office, Contractors, Other"
            },
            {
              "id": "a4",
              "name": "model",
              "type": "string",
              "value": "claude-haiku-4-5"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "search-receipt-emails",
      "name": "Search receipt emails",
      "type": "n8n-nodes-base.gmail",
      "position": [
        400,
        480
      ],
      "parameters": {
        "filters": {
          "q": "={{ $('Set config: search & categories').first().json.gmailQuery }}"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 2.1
    },
    {
      "id": "prepare-email-records",
      "name": "Prepare email records",
      "type": "n8n-nodes-base.code",
      "position": [
        752,
        480
      ],
      "parameters": {
        "jsCode": "// Flatten each matched email into a compact record the model can read.\nconst strip = (h) => String(h || '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/g, ' ').replace(/\\s+/g, ' ').trim();\nconst out = [];\nlet i = 0;\nfor (const it of $input.all()) {\n  const j = it.json;\n  const from = j.from?.value?.[0]?.address || j.From || j.from || '';\n  const subject = j.subject || j.Subject || '';\n  const date = j.date || j.internalDate || j.Date || '';\n  const body = strip(j.text || j.textPlain || j.snippet || j.textAsHtml || '');\n  out.push({ json: { i: i++, id: j.id || '', from, subject, date, body: body.slice(0, 1200) } });\n}\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "build-ai-extract-request",
      "name": "Build AI extract request",
      "type": "n8n-nodes-base.code",
      "position": [
        960,
        480
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: search & categories').first().json;\nconst emails = $input.all().map((it) => it.json);\nconst schema = { type: 'object', additionalProperties: false, required: ['receipts'], properties: {\n  receipts: { type: 'array', items: { type: 'object', additionalProperties: false,\n    required: ['i', 'is_receipt', 'vendor', 'amount', 'currency', 'date', 'category', 'invoice_no'], properties: {\n      i: { type: 'integer' }, is_receipt: { type: 'boolean' }, vendor: { type: 'string' },\n      amount: { type: 'number' }, currency: { type: 'string' }, date: { type: 'string' },\n      category: { type: 'string' }, invoice_no: { type: 'string' } } } } } };\nconst system = 'You extract structured expense data from receipt and invoice emails. For each email decide is_receipt (true only if it is an actual purchase receipt/invoice, not marketing). Extract vendor, total amount (number only), currency (ISO code, default ' + cfg.defaultCurrency + '), the purchase date as YYYY-MM-DD, an invoice/order number if present (else \"\"), and category chosen from: ' + cfg.categories + '. If a field is unknown use \"\" or 0. Do not invent amounts.';\nconst user = 'EMAILS (reference each by index i):\\n' +\n  emails.map((e) => '[' + e.i + '] From: ' + e.from + ' | Subject: ' + e.subject + ' | Date: ' + e.date + '\\n' + e.body).join('\\n\\n');\nreturn [{ json: { body: {\n  model: cfg.model, max_tokens: 2000, system,\n  messages: [{ role: 'user', content: user }],\n  output_config: { format: { type: 'json_schema', schema } },\n} } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "extract-receipts-with-claude",
      "name": "Extract receipts with Claude",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1360,
        480
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.ANTHROPIC_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "build-expense-rows",
      "name": "Build expense rows",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        480
      ],
      "parameters": {
        "jsCode": "const emails = $('Prepare email records').all().map((it) => it.json);\nconst blocks = ($json.content || []);\nconst text = (blocks.find((b) => b.type === 'text') || {}).text || '{\"receipts\":[]}';\nlet receipts = [];\ntry { receipts = (JSON.parse(text).receipts) || []; } catch (e) {}\nconst rows = receipts.filter((r) => r.is_receipt && (r.amount || r.vendor)).map((r) => {\n  const src = emails[r.i] || {};\n  return { json: {\n    date: r.date || '', vendor: r.vendor || '', amount: r.amount ?? '', currency: r.currency || '',\n    category: r.category || 'Other', invoice_no: r.invoice_no || '',\n    email_subject: src.subject || '', email_from: src.from || '',\n  } };\n});\nreturn rows.length ? rows : [];"
      },
      "typeVersion": 2
    },
    {
      "id": "append-to-expense-sheet",
      "name": "Append to expense sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1792,
        480
      ],
      "parameters": {
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 4.5
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "ee8acf9f-22c1-4bee-8a65-5004805f43a7",
  "nodeGroups": [],
  "connections": {
    "Every day": {
      "main": [
        [
          {
            "node": "Set config: search & categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build expense rows": {
      "main": [
        [
          {
            "node": "Append to expense sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare email records": {
      "main": [
        [
          {
            "node": "Build AI extract request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search receipt emails": {
      "main": [
        [
          {
            "node": "Prepare email records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI extract request": {
      "main": [
        [
          {
            "node": "Extract receipts with Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract receipts with Claude": {
      "main": [
        [
          {
            "node": "Build expense rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set config: search & categories": {
      "main": [
        [
          {
            "node": "Search receipt emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}