{
  "name": "AP Invoice Fraud Detection",
  "nodes": [
    {
      "id": "sticky-overview", "name": "Overview", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [-240, -500],
      "parameters": { "content": "## AP Invoice Fraud Detection\nVersion 1.0.0 — Finance\n\nProcesses incoming invoices from Gmail, extracts structured data with AI, checks Supabase for duplicate invoice numbers, validates vendor payment history, runs a risk assessment, and routes high-risk invoices to a Slack alert before logging all decisions.\n\nFlow: Gmail Trigger => Extract Invoice Data (AI Agent) => Check Duplicates (Supabase) => Count Duplicates (Code) => Check Vendor History (Supabase) => Merge All Context (Code) => Assess Fraud Risk (AI Agent) => High Risk? (IF) => Alert Slack + Log => Log to Supabase", "height": 260, "width": 700, "color": 7 }
    },
    {
      "id": "sticky-prereqs", "name": "Prerequisites", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [-240, -220],
      "parameters": { "content": "## Prerequisites\n- Gmail account receiving invoices (Gmail OAuth2 credential)\n- OpenAI API key (GPT-4o)\n- Supabase project with two tables:\n  - invoices: invoice_number, vendor_name, amount, status, created_at\n  - vendors: vendor_name, total_invoices, avg_amount, last_invoice_date, flagged\n- Slack workspace and channel for fraud alerts (Slack OAuth2 or Bot Token)\n- Gmail for sending hold notifications (reuse same credential)", "height": 200, "width": 420, "color": 5 }
    },
    {
      "id": "sticky-setup", "name": "Setup Required", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [200, -220],
      "parameters": { "content": "## Setup Required\n1. Gmail Trigger — connect Gmail OAuth2 credential\n2. Extract Invoice Data — connect OpenAI credential\n3. Check Duplicates + Check Vendor History — connect Supabase API credential, update table names\n4. Assess Fraud Risk — connect OpenAI credential\n5. Alert Slack — connect Slack credential, update #invoice-alerts channel name\n6. Send Hold Notice — connect Gmail OAuth2 credential, update your AP manager email\n7. Log Invoice + Log Fraud Flag — connect Supabase API credential", "height": 200, "width": 420, "color": 3 }
    },
    {
      "id": "sticky-howitworks", "name": "How It Works", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [-240, 0],
      "parameters": { "content": "## How It Works\n1. Gmail Trigger detects new invoice emails (attach PDF or include invoice details in body)\n2. AI Agent extracts: invoice number, vendor, amount, invoice date, due date, line items\n3. Supabase checked for duplicate invoice numbers — Count Duplicates Code node prevents item multiplication\n4. Supabase checked for vendor payment history — Merge All Context Code node consolidates everything\n5. Second AI Agent scores fraud risk (low/medium/high/critical) with reasoning\n6. IF risk >= high: post Slack alert + send hold notice to AP team via email\n7. Invoice logged to Supabase invoices table regardless of risk outcome", "height": 200, "width": 500, "color": 4 }
    },

    {
      "id": "gmail-trigger", "name": "Gmail — Invoice Inbox", "type": "n8n-nodes-base.gmailTrigger", "typeVersion": 1.2, "position": [0, 100],
      "parameters": { "pollTimes": { "item": [{ "mode": "everyMinute" }] }, "filters": { "includeSpamTrash": false }, "options": { "downloadAttachments": true } },
      "credentials": { "gmailOAuth2": { "id": "gmail-cred", "name": "Gmail OAuth2" } }
    },

    {
      "id": "sni-extract", "name": "Extract note", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [265, -35],
      "parameters": { "content": "AI extracts structured invoice data from email subject, body, and attachments", "height": 60, "width": 270 }
    },
    {
      "id": "extract-agent", "name": "Extract Invoice Data", "type": "@n8n/n8n-nodes-langchain.agent", "typeVersion": 1.8, "position": [260, 100],
      "parameters": {
        "agentType": "toolsAgent",
        "promptType": "define",
        "text": "=Extract all invoice data from this email.\n\nFrom: {{ $json.from }}\nSubject: {{ $json.subject }}\nBody: {{ $json.text || $json.snippet }}\n\nExtract the invoice number, vendor/supplier name, total amount (as a number), currency, invoice date, due date, and a list of line items if visible. If any field is not present, use null.",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are an accounts payable assistant. Extract structured invoice data precisely from email content. Invoice numbers typically follow patterns like INV-12345, 2024-001, or similar. Amounts should be numeric only (no currency symbols). Dates should be ISO 8601 format (YYYY-MM-DD). If the email does not appear to be an invoice, set is_invoice to false.",
          "returnIntermediateSteps": false
        }
      }
    },
    {
      "id": "extract-openai", "name": "OpenAI — Extract", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "typeVersion": 1.2, "position": [260, 300],
      "parameters": { "model": { "__rl": true, "value": "gpt-4o", "mode": "list" }, "options": { "temperature": 0 } },
      "credentials": { "openAiApi": { "id": "openai-cred", "name": "OpenAI API" } }
    },
    {
      "id": "extract-parser", "name": "Invoice Data Schema", "type": "@n8n/n8n-nodes-langchain.outputParserStructured", "typeVersion": 1.2, "position": [460, 300],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\"type\":\"object\",\"properties\":{\"invoice_number\":{\"type\":\"string\"},\"vendor_name\":{\"type\":\"string\"},\"amount\":{\"type\":\"number\"},\"currency\":{\"type\":\"string\"},\"invoice_date\":{\"type\":\"string\"},\"due_date\":{\"type\":\"string\"},\"line_items\":{\"type\":\"array\",\"items\":{\"type\":\"object\"}},\"is_invoice\":{\"type\":\"boolean\"}},\"required\":[\"invoice_number\",\"vendor_name\",\"amount\",\"currency\",\"is_invoice\"]}"
      }
    },

    {
      "id": "sni-dup", "name": "Duplicates note", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [525, -35],
      "parameters": { "content": "Queries invoices table for existing records with same invoice number", "height": 60, "width": 260 }
    },
    {
      "id": "check-duplicates", "name": "Check Duplicates", "type": "n8n-nodes-base.supabase", "typeVersion": 1, "position": [520, 100],
      "parameters": {
        "operation": "getAll",
        "tableId": "invoices",
        "returnAll": true,
        "filters": {
          "conditions": [
            { "keyName": "invoice_number", "keyValue": "={{ $json.output.invoice_number }}", "condition": "eq" }
          ]
        }
      },
      "credentials": { "supabaseApi": { "id": "supabase-cred", "name": "Supabase API" } },
      "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 2000, "onError": "continueErrorOutput"
    },

    {
      "id": "sni-count-dup", "name": "Count Dup note", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [785, -35],
      "parameters": { "content": "Consolidates duplicate rows into a single item, counts matches", "height": 60, "width": 255 }
    },
    {
      "id": "count-duplicates", "name": "Count Duplicates", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [780, 100],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const dupRows = $input.all();\nconst invoice = $('Extract Invoice Data').first().json.output || {};\nreturn [{ json: {\n  ...invoice,\n  duplicate_count: dupRows.filter(r => !r.json.error).length\n}}];"
      }
    },

    {
      "id": "sni-vendor", "name": "Vendor note", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [1045, -35],
      "parameters": { "content": "Queries vendor history to detect unusual amounts or flagged suppliers", "height": 60, "width": 260 }
    },
    {
      "id": "check-vendor", "name": "Check Vendor History", "type": "n8n-nodes-base.supabase", "typeVersion": 1, "position": [1040, 100],
      "parameters": {
        "operation": "getAll",
        "tableId": "vendors",
        "returnAll": true,
        "filters": {
          "conditions": [
            { "keyName": "vendor_name", "keyValue": "={{ $json.vendor_name }}", "condition": "eq" }
          ]
        }
      },
      "credentials": { "supabaseApi": { "id": "supabase-cred", "name": "Supabase API" } },
      "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 2000, "onError": "continueErrorOutput"
    },

    {
      "id": "sni-merge", "name": "Merge Context note", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [1305, -35],
      "parameters": { "content": "Merges invoice data, duplicate count, and vendor history into one item for risk assessment", "height": 60, "width": 270 }
    },
    {
      "id": "merge-context", "name": "Merge All Context", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1300, 100],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const vendorRows = $input.all();\nconst invoiceCtx = $('Count Duplicates').first().json;\nconst vendors = vendorRows.filter(r => !r.json.error).map(r => r.json);\nconst vendor = vendors[0] || null;\nreturn [{ json: {\n  ...invoiceCtx,\n  vendor_known: !!vendor,\n  vendor_avg_amount: vendor ? vendor.avg_amount : null,\n  vendor_flagged: vendor ? (vendor.flagged || false) : false,\n  vendor_total_invoices: vendor ? vendor.total_invoices : 0,\n  vendor_last_invoice_date: vendor ? vendor.last_invoice_date : null,\n  amount_deviation_pct: vendor && vendor.avg_amount > 0\n    ? Math.round(((invoiceCtx.amount - vendor.avg_amount) / vendor.avg_amount) * 100)\n    : null\n}}];"
      }
    },

    {
      "id": "sni-risk", "name": "Risk note", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [1565, -35],
      "parameters": { "content": "AI Agent scores fraud risk using duplicate count, vendor history, and amount deviation", "height": 60, "width": 270 }
    },
    {
      "id": "risk-agent", "name": "Assess Fraud Risk", "type": "@n8n/n8n-nodes-langchain.agent", "typeVersion": 1.8, "position": [1560, 100],
      "parameters": {
        "agentType": "toolsAgent",
        "promptType": "define",
        "text": "=Assess the fraud risk for this invoice.\n\nInvoice Number: {{ $json.invoice_number }}\nVendor: {{ $json.vendor_name }}\nAmount: {{ $json.amount }} {{ $json.currency }}\nInvoice Date: {{ $json.invoice_date }}\nDue Date: {{ $json.due_date }}\n\nDuplicate Invoice Count (same number already in system): {{ $json.duplicate_count }}\nVendor Known in System: {{ $json.vendor_known }}\nVendor Previously Flagged: {{ $json.vendor_flagged }}\nVendor Average Invoice Amount: {{ $json.vendor_avg_amount }}\nAmount Deviation from Vendor Average: {{ $json.amount_deviation_pct }}%\nVendor Total Historical Invoices: {{ $json.vendor_total_invoices }}\n\nEvaluate the risk of fraud or payment error and provide a risk score and recommended action.",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are an accounts payable fraud detection specialist. Evaluate invoices for: duplicate submissions, amounts significantly above vendor average (>50% deviation = high risk, >100% = critical), unknown vendors, previously flagged vendors, or suspicious timing patterns. Risk levels: low = approve automatically, medium = flag for review, high = hold pending approval, critical = block and escalate immediately. Always provide clear reasoning.",
          "returnIntermediateSteps": false
        }
      }
    },
    {
      "id": "risk-openai", "name": "OpenAI — Risk", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "typeVersion": 1.2, "position": [1560, 300],
      "parameters": { "model": { "__rl": true, "value": "gpt-4o", "mode": "list" }, "options": { "temperature": 0 } },
      "credentials": { "openAiApi": { "id": "openai-cred", "name": "OpenAI API" } }
    },
    {
      "id": "risk-parser", "name": "Fraud Risk Schema", "type": "@n8n/n8n-nodes-langchain.outputParserStructured", "typeVersion": 1.2, "position": [1760, 300],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\"type\":\"object\",\"properties\":{\"risk_level\":{\"type\":\"string\",\"enum\":[\"low\",\"medium\",\"high\",\"critical\"]},\"risk_score\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":100},\"fraud_indicators\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"reasoning\":{\"type\":\"string\"},\"recommended_action\":{\"type\":\"string\"},\"requires_hold\":{\"type\":\"boolean\"}},\"required\":[\"risk_level\",\"risk_score\",\"fraud_indicators\",\"reasoning\",\"recommended_action\",\"requires_hold\"]}"
      }
    },

    {
      "id": "sni-high-risk", "name": "High Risk note", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [1825, -35],
      "parameters": { "content": "Routes to Slack alert path if risk requires hold, otherwise logs directly", "height": 60, "width": 260 }
    },
    {
      "id": "high-risk-check", "name": "High Risk?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [1820, 100],
      "parameters": {
        "conditions": {
          "options": { "typeValidation": "loose" },
          "conditions": [{ "id": "cond-risk", "leftValue": "={{ $json.output.requires_hold }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equal" } }],
          "combinator": "and"
        }
      }
    },

    {
      "id": "sni-slack", "name": "Slack note", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [2085, -115],
      "parameters": { "content": "Posts fraud alert to #invoice-alerts Slack channel with full details", "height": 60, "width": 255 }
    },
    {
      "id": "alert-slack", "name": "Alert Slack", "type": "n8n-nodes-base.slack", "typeVersion": 2.2, "position": [2080, -80],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": { "__rl": true, "value": "#invoice-alerts", "mode": "name" },
        "text": "=*FRAUD ALERT — {{ $('Merge All Context').first().json.risk_level.toUpperCase() }} RISK*\n\n*Invoice:* {{ $('Merge All Context').first().json.invoice_number }}\n*Vendor:* {{ $('Merge All Context').first().json.vendor_name }}\n*Amount:* {{ $('Merge All Context').first().json.amount }} {{ $('Merge All Context').first().json.currency }}\n*Risk Score:* {{ $json.output.risk_score }}/100\n\n*Fraud Indicators:*\n{{ $json.output.fraud_indicators.join('\\n') }}\n\n*Reasoning:* {{ $json.output.reasoning }}\n\n*Recommended Action:* {{ $json.output.recommended_action }}",
        "otherOptions": {}
      },
      "credentials": { "slackApi": { "id": "slack-cred", "name": "Slack OAuth2" } },
      "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 2000, "onError": "continueErrorOutput"
    },
    {
      "id": "send-hold-notice", "name": "Send Hold Notice", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [2340, -80],
      "parameters": {
        "sendTo": "ap-manager@example.com",
        "subject": "=Invoice Hold — {{ $('Merge All Context').first().json.invoice_number }} from {{ $('Merge All Context').first().json.vendor_name }} ({{ $('Merge All Context').first().json.risk_level.toUpperCase() }} RISK)",
        "emailType": "html",
        "message": "=<h2>Invoice Flagged for Review</h2><p>The following invoice has been placed on hold pending your approval.</p><table border='1' cellpadding='6' style='border-collapse:collapse'><tr><td><b>Invoice Number</b></td><td>{{ $('Merge All Context').first().json.invoice_number }}</td></tr><tr><td><b>Vendor</b></td><td>{{ $('Merge All Context').first().json.vendor_name }}</td></tr><tr><td><b>Amount</b></td><td>{{ $('Merge All Context').first().json.amount }} {{ $('Merge All Context').first().json.currency }}</td></tr><tr><td><b>Risk Level</b></td><td>{{ $('Assess Fraud Risk').first().json.output.risk_level.toUpperCase() }}</td></tr><tr><td><b>Risk Score</b></td><td>{{ $('Assess Fraud Risk').first().json.output.risk_score }}/100</td></tr></table><p><b>Fraud Indicators:</b><br>{{ $('Assess Fraud Risk').first().json.output.fraud_indicators.join('<br>') }}</p><p><b>Reasoning:</b> {{ $('Assess Fraud Risk').first().json.output.reasoning }}</p><p><b>Recommended Action:</b> {{ $('Assess Fraud Risk').first().json.output.recommended_action }}</p>",
        "options": {}
      },
      "credentials": { "gmailOAuth2": { "id": "gmail-cred", "name": "Gmail OAuth2" } },
      "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 2000, "onError": "continueErrorOutput"
    },

    {
      "id": "sni-log-invoice", "name": "Log Invoice note", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [2605, -35],
      "parameters": { "content": "Logs every processed invoice to Supabase for record keeping", "height": 60, "width": 255 }
    },
    {
      "id": "log-invoice", "name": "Log Invoice to Supabase", "type": "n8n-nodes-base.supabase", "typeVersion": 1, "position": [2600, 100],
      "parameters": {
        "operation": "insert",
        "tableId": "invoices",
        "fieldsUi": {
          "fieldValues": [
            { "fieldId": "invoice_number",  "fieldValue": "={{ $('Merge All Context').first().json.invoice_number }}" },
            { "fieldId": "vendor_name",     "fieldValue": "={{ $('Merge All Context').first().json.vendor_name }}" },
            { "fieldId": "amount",          "fieldValue": "={{ $('Merge All Context').first().json.amount }}" },
            { "fieldId": "currency",        "fieldValue": "={{ $('Merge All Context').first().json.currency }}" },
            { "fieldId": "invoice_date",    "fieldValue": "={{ $('Merge All Context').first().json.invoice_date }}" },
            { "fieldId": "due_date",        "fieldValue": "={{ $('Merge All Context').first().json.due_date }}" },
            { "fieldId": "risk_level",      "fieldValue": "={{ $('Assess Fraud Risk').first().json.output.risk_level }}" },
            { "fieldId": "risk_score",      "fieldValue": "={{ $('Assess Fraud Risk').first().json.output.risk_score }}" },
            { "fieldId": "on_hold",         "fieldValue": "={{ $('Assess Fraud Risk').first().json.output.requires_hold }}" },
            { "fieldId": "status",          "fieldValue": "={{ $('Assess Fraud Risk').first().json.output.requires_hold ? 'on_hold' : 'approved' }}" },
            { "fieldId": "created_at",      "fieldValue": "={{ DateTime.now().toISO() }}" }
          ]
        }
      },
      "credentials": { "supabaseApi": { "id": "supabase-cred", "name": "Supabase API" } },
      "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 2000, "onError": "continueErrorOutput"
    }
  ],
  "connections": {
    "Gmail — Invoice Inbox":    { "main": [[{ "node": "Extract Invoice Data",   "type": "main", "index": 0 }]] },
    "OpenAI — Extract":         { "ai_languageModel": [[{ "node": "Extract Invoice Data", "type": "ai_languageModel", "index": 0 }]] },
    "Invoice Data Schema":      { "ai_outputParser":  [[{ "node": "Extract Invoice Data", "type": "ai_outputParser",  "index": 0 }]] },
    "Extract Invoice Data":     { "main": [[{ "node": "Check Duplicates",       "type": "main", "index": 0 }]] },
    "Check Duplicates":         { "main": [[{ "node": "Count Duplicates",       "type": "main", "index": 0 }]] },
    "Count Duplicates":         { "main": [[{ "node": "Check Vendor History",   "type": "main", "index": 0 }]] },
    "Check Vendor History":     { "main": [[{ "node": "Merge All Context",      "type": "main", "index": 0 }]] },
    "Merge All Context":        { "main": [[{ "node": "Assess Fraud Risk",      "type": "main", "index": 0 }]] },
    "OpenAI — Risk":            { "ai_languageModel": [[{ "node": "Assess Fraud Risk", "type": "ai_languageModel", "index": 0 }]] },
    "Fraud Risk Schema":        { "ai_outputParser":  [[{ "node": "Assess Fraud Risk", "type": "ai_outputParser",  "index": 0 }]] },
    "Assess Fraud Risk":        { "main": [[{ "node": "High Risk?",             "type": "main", "index": 0 }]] },
    "High Risk?": {
      "main": [
        [{ "node": "Alert Slack",       "type": "main", "index": 0 }],
        [{ "node": "Log Invoice to Supabase", "type": "main", "index": 0 }]
      ]
    },
    "Alert Slack":              { "main": [[{ "node": "Send Hold Notice",        "type": "main", "index": 0 }]] },
    "Send Hold Notice":         { "main": [[{ "node": "Log Invoice to Supabase", "type": "main", "index": 0 }]] }
  },
  "pinData": {},
  "settings": { "executionOrder": "v1", "saveManualExecutions": true, "callerPolicy": "workflowsFromSameOwner" },
  "staticData": null,
  "tags": ["Finance", "Fraud Detection", "Gmail", "Supabase", "Slack", "AI"],
  "triggerCount": 1,
  "active": false
}
