OCR, Claude AI, Slack 및 Notion DB 기반 자동화된 청구서 결제 추적
고급
이것은AI Summarization, Multimodal AI분야의자동화 워크플로우로, 92개의 노드를 포함합니다.주로 If, Code, Wait, Slack, Notion 등의 노드를 사용하며. OCR, Claude AI, Slack, Notion DB 기반 자동 인voice 결제 추적
사전 요구사항
- •Slack Bot Token 또는 Webhook URL
- •Notion API Key
- •대상 API의 인증 정보가 필요할 수 있음
- •Anthropic API Key
사용된 노드 (92)
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
"meta": {
"instanceId": "c8523726fb538ba356528013ec3b00a89ffd0cddf2d31a7bcce94063a36b7fec",
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "909b0322-55e4-4c3d-a5a9-9392d5bbf745",
"name": "Format Check",
"type": "n8n-nodes-base.code",
"position": [
-5808,
1744
],
"parameters": {
"jsCode": "const files = $('Slack Trigger').first().json.files || [];\nconst results = [];\n\nfor (const file of files) {\n const mimetype = file.mimetype || \"\";\n const fileType = mimetype.split('/').pop().toLowerCase();\n const fileName = file.name || file.title || \"unknown\";\n\n const isImage = ['jpg', 'jpeg', 'png', 'webp'].includes(fileType);\n const isDocument = ['pdf', 'doc', 'docx', 'txt', 'rtf', 'eml'].includes(fileType);\n const isEmail = fileType === 'eml' || fileName.endsWith('.eml');\n\n results.push({\n json: {\n ...file, // keep Slack file data like url_private_download\n fileType,\n mimetype,\n isImage,\n isDocument,\n isEmail,\n }\n });\n}\n\nreturn results;\n"
},
"typeVersion": 2
},
{
"id": "ca8559e4-b29b-4a1a-b113-e70bba47f23a",
"name": "Check Format",
"type": "n8n-nodes-base.switch",
"position": [
-5584,
1744
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Image",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f5d6e124-c68e-42bb-a9bb-c97d54b09143",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.isImage }}",
"rightValue": "true"
}
]
},
"renameOutput": true
},
{
"outputKey": "Document",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "87759194-16d1-4593-8c9b-016aa7be4e03",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.isDocument }}",
"rightValue": "true"
}
]
},
"renameOutput": true
}
]
},
"options": {
"allMatchingOutputs": true
}
},
"typeVersion": 3.2
},
{
"id": "6543e986-8860-457d-a8de-bb052718e9c6",
"name": "Take Binary Files for Document",
"type": "n8n-nodes-base.httpRequest",
"position": [
-5360,
1840
],
"parameters": {
"url": "={{ $json.url_private_download }}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "slackApi"
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 4.2
},
{
"id": "af88d22a-5a2d-4471-9ac9-b1cb9531bd35",
"name": "OCR Space Parse1",
"type": "n8n-nodes-base.httpRequest",
"position": [
-5136,
1840
],
"parameters": {
"url": "https://api.ocr.space/parse/image",
"method": "POST",
"options": {
"timeout": 100000
},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "filetype",
"value": "PDF"
},
{
"name": "file",
"parameterType": "formBinaryData",
"inputDataFieldName": "data"
},
{
"name": "scale",
"value": "true"
},
{
"name": "OCREngine",
"value": "2"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"id": "dkJjl1msBNIeIT5u",
"name": "OCR Space"
}
},
"retryOnFail": true,
"typeVersion": 4.2,
"waitBetweenTries": 5000
},
{
"id": "ecb68486-32af-41e3-a9a7-86c763205061",
"name": "코드",
"type": "n8n-nodes-base.code",
"position": [
-4688,
1936
],
"parameters": {
"jsCode": "// $input.all() gives you the 3 separate OCR result items\nconst inputItems = $input.all();\n\nreturn inputItems.map((item, index) => {\n const parsedResults = item.json.ParsedResults || [];\n\n // Join all ParsedText per result into one string\n const mergedText = parsedResults\n .map(p => p.ParsedText || \"\")\n .join(\"\\n\\n\")\n .trim();\n\n return {\n json: {\n index: index + 1,\n mergedParsedText: mergedText\n }\n };\n});\n"
},
"typeVersion": 2
},
{
"id": "acb3e33c-59de-4cf3-a008-a47af9673abb",
"name": "Check parsing Error3",
"type": "n8n-nodes-base.if",
"position": [
-4912,
1840
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "ddbbbfbb-e05b-4ca9-b607-65ad8ac748bb",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $('OCR Space Parse1').item.json.IsErroredOnProcessing }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "96e61695-6dfd-4cf4-8064-aea9d80284f0",
"name": "Basic LLM 체인",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
-4240,
1888
],
"parameters": {
"text": "=Extract the invoice fields from this OCR text and populate the EXACT schema.\n\nTARGET SCHEMA:\n{\n \"invoice_no\": \"\",\n \"vendor\": \"\",\n \"issue_date\": \"YYYY-MM-DD\",\n \"due_date\": \"YYYY-MM-DD\",\n \"currency\": \"\",\n \"subtotal\": 0,\n \"tax_total\": 0,\n \"discount_total\": 0,\n \"discount_percent\": 0,\n \"amount_total\": 0,\n\n \"amount_due\": 0, // if shown (a.k.a. Balance Due); else 0\n \"paid_amount\": 0, // if shown or implied; else 0\n \"paid_date\": \"\", // if shown; else \"\"\n \"receipt_no\": \"\", // if shown; else \"\"\n \"payment_method\": \"\", // if shown; else \"\"\n\n \"destination_account\": \"\",\n \"payment_ref\": \"\",\n \"doc_says_paid\": false,\n \"status\": \"\",\n \"notes\": \"\",\n \"source_file_id\": \"\",\n \"source_file_name\": \"\",\n \"source_file_url\": \"\",\n \"ingestion_batch\": \"\",\n \"line_items\": [],\n \"attachments\": []\n}\n\nOCR TEXT:\n{{ $json.OCRResult }}\n",
"batching": {},
"messages": {
"messageValues": [
{
"message": "=You are an extraction engine for US invoices. Output EXACTLY the JSON fields of the target schema—no extra keys, no comments, no markdown. Do not invent values.\n\nGeneral rules\n- Locale: US. Normalize dates to YYYY-MM-DD.\n- Numbers: plain decimals (no symbols/commas).\n- currency: infer from symbol/labels when present (“$” ⇒ “USD”), else \"\".\n- If a field is unavailable: use \"\" for strings, 0 for numbers, false for booleans, [] for arrays.\n\nDiscount fields\n- If the doc shows a percent (e.g., “Discount 49%”), set discount_percent to the decimal (0.49) and discount_total to 0 unless a currency discount is also printed.\n- If the doc shows a currency discount (e.g., “Discount $60.27”), set discount_total to that amount; discount_percent is 0 unless the percent is explicitly printed too.\n- If both forms are printed, fill both.\n\nTotals\n- amount_total: prefer the document’s explicit “Amount due/Total/Total amount/Grand total”.\n- If not printed but subtotal, discount_* and tax_total are present, compute:\n amount_total = subtotal − (discount_total OR subtotal × discount_percent) + tax_total.\n- Otherwise leave amount_total = 0.\n\nPartial payments\n- If the doc shows Amount due / Balance due:\n • Set amount_due to that value.\n • If amount_total is known and amount_due < amount_total, set paid_amount = amount_total − amount_due (≥ 0).\n- If the doc shows Amount paid / Payment history:\n • Sum payments and set paid_amount.\n • If amount_due is not shown but amount_total is known, set amount_due = max(amount_total − paid_amount, 0).\n- If a paid date, receipt number, or payment method is explicitly shown, set paid_date, receipt_no, payment_method; otherwise leave them empty.\n- Clamp impossible values (no negatives; when amount_total is known, neither paid_amount nor amount_due may exceed it).\n\nDocument-based status\n- If amount_due == 0 AND the document states PAID (or shows “Amount due $0.00”): status = \"Paid (Unverified)\".\n- Else if paid_amount > 0 AND amount_due > 0: status = \"Partially Paid\".\n- Else: status = \"Unpaid\".\n- Set doc_says_paid = true only if the document itself explicitly says “PAID” or shows “Amount due $0.00”; otherwise false.\n\nLine items\n- line_items: include when clearly listed, as\n [{\"description\": string, \"qty\": number, \"unit_price\": number, \"amount\": number}], else [].\n\nAttachments\n- attachments: [] unless explicit links are present.\n\nReturn ONLY the JSON object matching the target schema.\n"
}
]
},
"promptType": "define"
},
"retryOnFail": false,
"typeVersion": 1.7
},
{
"id": "0aaa9bdf-05b2-4dc1-ad6d-4de8926ef9f1",
"name": "Anthropic 채팅 모델4",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
-4240,
2048
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-3-5-haiku-20241022",
"cachedResultName": "Claude Haiku 3.5"
},
"options": {}
},
"credentials": {
"anthropicApi": {
"id": "UBlNqbwvx1EotIn7",
"name": "Anthropic account"
}
},
"typeVersion": 1.3
},
{
"id": "54187b60-dabd-42a7-b5c9-d46199dbed73",
"name": "Cleans AI Response",
"type": "n8n-nodes-base.code",
"position": [
-3888,
1888
],
"parameters": {
"jsCode": "// n8n Code node — Run Once for All Items\n// Repair + Enforce schema + attach Slack file info\n// Adds discount_percent support + derives discount_amount & stable amount_total\n// + Partial payment normalization (amount_due, paid_amount, paid_date, receipt_no, payment_method)\n\nfunction stripCodeFences(s) {\n return String(s || \"\")\n .replace(/```(?:json)?/gi, \"\")\n .replace(/```/g, \"\")\n .trim();\n}\n\nfunction findTopLevelJson(s) {\n const str = String(s || \"\");\n let start = -1, depth = 0;\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n if (start === -1) {\n if (ch === \"{\" || ch === \"[\") { start = i; depth = 1; }\n } else {\n if (ch === \"{\" || ch === \"[\") depth++;\n if (ch === \"}\" || ch === \"]\") depth--;\n if (depth === 0) return str.slice(start, i + 1);\n }\n }\n return null;\n}\n\nfunction safeParseAny(x) {\n if (Array.isArray(x)) return x;\n if (x && typeof x === \"object\" && (x.invoice_no || x.vendor || x.line_items)) return x;\n\n let txt = \"\";\n if (typeof x === \"string\") txt = x;\n else if (typeof x?.text === \"string\") txt = x.text;\n else if (typeof x?.output === \"string\") txt = x.output;\n else if (typeof x?.response === \"string\") txt = x.response;\n else if (typeof x?.content === \"string\") txt = x.content;\n else if (typeof x?.message === \"string\") txt = x.message;\n else txt = JSON.stringify(x ?? \"\");\n\n txt = stripCodeFences(txt);\n\n try { return JSON.parse(txt); } catch {}\n const block = findTopLevelJson(txt);\n if (!block) throw new Error(\"No JSON found in LLM output\");\n return JSON.parse(block);\n}\n\n// coercers\nfunction num(v) {\n if (v === \"\" || v == null || (typeof v === \"number\" && !isFinite(v))) return 0;\n if (typeof v === \"number\") return v;\n let s = String(v).trim();\n if (!s) return 0;\n // keep digits , . -\n s = s.replace(/[^\\d,.\\-]/g, \"\");\n if (s.includes(\",\") && s.includes(\".\")) s = s.replace(/,/g, \"\");\n else if (s.includes(\",\") && !s.includes(\".\")) {\n if ((s.match(/,/g) || []).length === 1 && /\\d,\\d{1,2}$/.test(s)) s = s.replace(\",\", \".\");\n else s = s.replace(/,/g, \"\");\n }\n const n = Number(s);\n return isFinite(n) ? n : 0;\n}\nconst str = (v) => (v == null ? \"\" : String(v).trim());\nconst bool = (v) => v === true;\nconst two = (n) => Math.round((Number(n) || 0) * 100) / 100;\n\n// parse percent from \"49%\", \"49\", 0.49, etc. → 0..1\nfunction pct(v) {\n if (v == null || v === \"\") return 0;\n if (typeof v === \"string\" && v.includes(\"%\")) return Math.min(Math.max(num(v) / 100, 0), 1);\n let n = num(v);\n if (n > 1) n = n / 100;\n if (n < 0) n = 0;\n if (n > 1) n = 1;\n return n;\n}\n\nfunction normalizeOne(obj, fileUrl) {\n const line_items = Array.isArray(obj?.line_items)\n ? obj.line_items.map(x => ({\n description: str(x?.description),\n qty: num(x?.qty),\n unit_price: num(x?.unit_price),\n amount: num(x?.amount)\n }))\n : [];\n\n // base fields from LLM\n const base = {\n invoice_no: str(obj?.invoice_no),\n vendor: str(obj?.vendor),\n issue_date: str(obj?.issue_date),\n due_date: str(obj?.due_date),\n currency: str(obj?.currency),\n subtotal: num(obj?.subtotal),\n tax_total: num(obj?.tax_total),\n\n // discounts (raw)\n discount_total: num(obj?.discount_total), // fixed amount if present\n discount_percent: pct(obj?.discount_percent), // normalized 0..1\n\n amount_total: num(obj?.amount_total),\n\n // payment/receipt extras (raw; normalized below)\n amount_due: num(obj?.amount_due),\n paid_amount: num(obj?.paid_amount),\n paid_date: str(obj?.paid_date),\n receipt_no: str(obj?.receipt_no),\n payment_method: str(obj?.payment_method),\n\n destination_account: str(obj?.destination_account),\n payment_ref: str(obj?.payment_ref),\n doc_says_paid: bool(obj?.doc_says_paid),\n status: str(obj?.status),\n notes: str(obj?.notes),\n source_file_id: str(obj?.source_file_id),\n source_file_name: str(obj?.source_file_name),\n source_file_url: str(obj?.source_file_url),\n ingestion_batch: str(obj?.ingestion_batch),\n line_items,\n attachments: Array.isArray(obj?.attachments) ? obj.attachments.map(str) : []\n };\n\n // ---- discount math ----\n const discount_amount =\n base.discount_total > 0\n ? two(base.discount_total)\n : two(base.subtotal * (base.discount_percent || 0));\n\n // compute fallback amount_total\n const calc_total = two(base.subtotal - discount_amount + base.tax_total);\n\n // choose stable amount_total for pipeline (prefer LLM if sane; else calc)\n const provided = base.amount_total;\n const useProvided =\n provided > 0 && Math.abs(provided - calc_total) <= Math.max(0.01, calc_total * 0.01);\n base.amount_total = useProvided ? two(provided) : calc_total;\n\n // Notion percent wants 0..1\n base.discount_percent_for_notion = +(base.discount_percent || 0).toFixed(4);\n base.discount_amount = discount_amount; // handy for dedup & debug\n base._amount_total_source = useProvided ? \"provided\" : \"calculated\";\n\n // ---- partial / payment normalization ----\n const at = Number(base.amount_total || 0);\n let due = Number(base.amount_due || 0);\n let paid = Number(base.paid_amount || 0);\n\n // Derive missing pieces when amount_total is known\n if (at > 0) {\n if (paid > 0 && due <= 0) due = Math.max(0, two(at - paid));\n if (due > 0 && paid <= 0) paid = Math.max(0, two(at - due));\n // If doc says PAID and due is zero but paid missing, assume fully paid\n if (base.doc_says_paid && due === 0 && paid <= 0) paid = at;\n // Clamp impossible values\n if (paid < 0) paid = 0;\n if (due < 0) due = 0;\n if (paid > at) paid = at;\n if (due > at) due = at;\n if (paid + due > at) due = Math.max(0, two(at - paid));\n } else {\n // amount_total unknown — still clamp non-negatives\n if (paid < 0) paid = 0;\n if (due < 0) due = 0;\n }\n\n base.paid_amount = two(paid);\n base.amount_due = two(due);\n\n // Receipt hints payload for later routing/Notion Receipt DB\n base.receipt_hints = {\n paid_amount: base.paid_amount,\n paid_date: base.paid_date,\n receipt_no: base.receipt_no,\n payment_method: base.payment_method\n };\n\n // Nudge status if inconsistent with amounts\n if (at > 0) {\n if (base.amount_due === 0 && (base.doc_says_paid || base.paid_amount >= at - 0.01)) {\n base.status = \"Paid (Unverified)\";\n } else if (base.paid_amount > 0 && base.amount_due > 0) {\n base.status = \"Partially Paid\";\n } else if (base.paid_amount === 0 && base.amount_due >= at - 0.01) {\n base.status = \"Unpaid\";\n }\n }\n\n // ---- Attach Slack file URL (if available) ----\n if (fileUrl) {\n let fileName = \"\", fileId = \"\";\n try {\n const u = new URL(fileUrl);\n const seg = u.pathname.split(\"/\").filter(Boolean);\n fileName = decodeURIComponent(seg[seg.length - 1] || \"\");\n const m = u.pathname.match(/-(F[A-Z0-9]+)\\//i);\n if (m) fileId = m[1];\n } catch {\n const m1 = fileUrl.match(/\\/download\\/([^?\\/#]+)($|\\?)/i);\n if (m1) fileName = decodeURIComponent(m1[1]);\n const m2 = fileUrl.match(/-(F[A-Z0-9]+)\\//i);\n if (m2) fileId = m2[1];\n }\n\n base.source_file_url = fileUrl;\n if (!base.source_file_name) base.source_file_name = fileName;\n if (!base.source_file_id) base.source_file_id = fileId;\n\n const atSet = new Set(base.attachments.map(String));\n atSet.add(fileUrl);\n base.attachments = Array.from(atSet);\n }\n\n // Guard status values\n const allowedStatus = new Set([\n \"Unpaid\",\"Paid (Unverified)\",\"Paid (Verified)\",\n \"Partially Paid\",\"Overdue\",\"Void/Cancelled\",\"Duplicate\"\n ]);\n if (!allowedStatus.has(base.status)) base.status = \"Unpaid\";\n\n // Basic date sanity\n const dateOk = (s) => /^(\\d{4})-(\\d{2})-(\\d{2})$/.test(s);\n if (base.issue_date && !dateOk(base.issue_date)) base.issue_date = \"\";\n if (base.due_date && !dateOk(base.due_date)) base.due_date = \"\";\n\n return base;\n}\n\n// --- Collect Slack file URLs from either node (PDF or Image lanes) ---\nfunction collectSlackUrls(nodeName) {\n try {\n return $(nodeName).all()\n .map(it =>\n String(\n it.json?.url_private_download ||\n it.json?.file?.url_private_download || // some Slack nodes nest under file\n \"\"\n )\n )\n .filter(Boolean);\n } catch {\n return [];\n }\n}\n\nconst urlsDoc = collectSlackUrls('Take Binary Files for Document'); // PDF lane\nconst urlsImg = collectSlackUrls('Take Binary Files'); // Image lane\n\n// Pick a URL for item index i; prefer same index, then first available\nfunction pickFileUrl(i) {\n return urlsDoc[i] || urlsImg[i] || urlsDoc[0] || urlsImg[0] || \"\";\n}\n\n// --- Process ALL incoming items; each may yield one or many outputs ---\nconst outputs = [];\n\nfor (const [idx, item] of $input.all().entries()) {\n let parsed = safeParseAny(item.json);\n\n if (parsed && !Array.isArray(parsed) && typeof parsed === \"object\") {\n const maybeArr = parsed.data || parsed.items || parsed.result || parsed.invoices;\n if (Array.isArray(maybeArr)) parsed = maybeArr;\n }\n if (!Array.isArray(parsed)) parsed = [parsed];\n\n const fileUrlForThisItem = pickFileUrl(idx);\n\n for (const obj of parsed) {\n const out = normalizeOne(obj, fileUrlForThisItem);\n outputs.push({ json: out });\n }\n}\n\nreturn outputs;\n"
},
"typeVersion": 2
},
{
"id": "0e729384-88a1-4ee3-9bd1-ccc218462dbc",
"name": "Internal Check Duplicate Invoice",
"type": "n8n-nodes-base.code",
"position": [
-3664,
1888
],
"parameters": {
"jsCode": "// Input: many cleaned invoices (each item.json is like your sample)\n// Output: clusters of duplicates; each cluster picks one \"keep\" and lists \"drop\"\n\nconst input = $input.all().map(i => i.json);\n\n// --- Helpers ---\nconst canonVendor = (v) => {\n if (!v) return \"\";\n return String(v)\n .toLowerCase()\n .replace(/[.,'’\"]/g, \" \")\n .replace(/\\b(pt|tbk|cv|inc|ltd|llc|llp|plc|corp|co|gmbh|sarl|bv)\\b/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n};\nconst canonInv = (s) => {\n if (!s) return \"\";\n return String(s)\n .toUpperCase()\n .replace(/\\s+/g, \"\")\n .replace(/[–—]/g, \"-\")\n .replace(/[^A-Z0-9\\/\\-\\.]/g, \"\")\n .replace(/-+/g, \"-\")\n .trim();\n};\nconst round2 = (n) => Math.round(Number(n || 0) * 100) / 100;\n\n// --- NEW: scoring helpers so PAID wins ---\nfunction parseYmd(s) {\n // \"YYYY-MM-DD\" -> epoch ms; invalid -> 0\n const m = String(s || \"\").match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n if (!m) return 0;\n return Date.UTC(+m[1], +m[2] - 1, +m[3]);\n}\n\nfunction statusRank(status, docPaid) {\n const s = String(status || \"\").toLowerCase();\n if (docPaid === true || /paid\\s*\\(verified\\)/i.test(status)) return 4;\n if (/paid/i.test(s)) return 3; // Paid (Unverified)\n if (/partially/i.test(s)) return 2; // Partially Paid\n if (/overdue/i.test(s)) return 1; // Overdue still beats Unpaid\n return 0; // Unpaid / others\n}\n\nfunction richness(o) {\n let r = 0;\n if (Array.isArray(o.attachments) && o.attachments.length) r += 2;\n if (o.source_file_id) r += 1;\n if (o.notes) r += 0.5;\n if (Array.isArray(o.line_items)) r += Math.min(2, o.line_items.length * 0.1);\n return r;\n}\n\n// choose one representative to keep\nfunction chooseMaster(arr) {\n let best = null, bestScore = -Infinity, bestWhy = \"\";\n for (const o of arr) {\n const paidRank = statusRank(o.status, o.doc_says_paid); // 0..4\n const newestDate = Math.max(parseYmd(o.due_date), parseYmd(o.issue_date));\n const rich = richness(o);\n\n // Weights: paidness >>> recency > richness\n const score =\n paidRank * 100 + // ensure any \"Paid\" beats \"Unpaid\"\n (newestDate / 8.64e7) * 0.1 + // days since epoch * 0.1\n rich; // small tie-break\n\n const why = `paidRank=${paidRank} doc_says_paid=${!!o.doc_says_paid} status=${o.status || \"\"} newest=${newestDate ? new Date(newestDate).toISOString().slice(0,10) : \"-\"} richness=${rich}`;\n\n if (score > bestScore) {\n best = o; bestScore = score; bestWhy = why;\n }\n }\n best._keep_reason_debug = bestWhy;\n best._keep_score_debug = bestScore;\n return best || arr[0];\n}\n\n// --- Build clusters ---\nconst clustersMap = new Map(); // key -> { reason, list: [] }\n\nfor (const o of input) {\n const vendor_canon = canonVendor(o.vendor);\n const invoice_no_canon = canonInv(o.invoice_no);\n const currency = (o.currency || \"\").toUpperCase().trim();\n const amt = round2(o.amount_total);\n const due = (o.due_date || o.issue_date || \"\").slice(0, 10);\n\n let key, reason;\n if (invoice_no_canon) {\n key = `INV#${vendor_canon}|${invoice_no_canon}`;\n reason = \"SAME_VENDOR + INVOICE_NO\";\n } else {\n key = `FALLBACK#${vendor_canon}|${currency}|${amt}|${due}`;\n reason = \"NO_INVOICE_NO → SAME_VENDOR + CURRENCY + AMOUNT + DATE\";\n }\n\n if (!clustersMap.has(key)) clustersMap.set(key, { reason, list: [] });\n clustersMap.get(key).list.push({\n ...o,\n _vendor_canon: vendor_canon,\n _invoice_no_canon: invoice_no_canon,\n _fallback_key: !invoice_no_canon\n });\n}\n\n// --- Emit only duplicate clusters (>1) ---\nconst results = [];\nfor (const [key, { reason, list }] of clustersMap.entries()) {\n if (list.length <= 1) continue;\n\n const amountSet = Array.from(new Set(list.map(x => round2(x.amount_total))));\n const currencySet = Array.from(new Set(list.map(x => (x.currency || \"\").toUpperCase().trim())));\n\n // annotate if amounts differ\n let reasonDetailed = reason;\n if (reason.startsWith(\"SAME_VENDOR\") && amountSet.length > 1) {\n reasonDetailed += \" (amount differs across copies)\";\n }\n\n // --- NEW: count paid vs unpaid; prefer paid in keep\n const paidCount = list.filter(x => x.doc_says_paid || /^paid/i.test(String(x.status || \"\"))).length;\n if (paidCount > 0) reasonDetailed += \" | keep prefers PAID copy\";\n\n const keep = chooseMaster(list);\n const drop = list.filter(x => x !== keep);\n\n results.push({\n json: {\n isDuplicate: true,\n cluster_key: key,\n reason: reasonDetailed,\n count: list.length,\n amounts: amountSet,\n currencies: currencySet,\n paid_count: paidCount, // debug/metrics\n keep, // should now be one of C/D (paid)\n drop,\n entries: list\n }\n });\n}\n\n// No duplicates? Emit a simple flag.\nif (results.length === 0) {\n return [{ json: { isDuplicate: false, info: \"No duplicate invoices found.\" } }];\n}\n\nreturn results;\n"
},
"typeVersion": 2
},
{
"id": "dbcea768-1818-4a07-932c-3161e06342b5",
"name": "Slack 트리거",
"type": "n8n-nodes-base.slackTrigger",
"position": [
-6032,
1744
],
"webhookId": "d13d3a70-faed-40f9-99fd-00a219c3af7c",
"parameters": {
"options": {},
"trigger": [
"message"
],
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09701BLY2Z",
"cachedResultName": "expenses"
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 1
},
{
"id": "0202318a-016b-4130-930c-bd092f18c492",
"name": "병합 with Original Data",
"type": "n8n-nodes-base.code",
"position": [
-3440,
1888
],
"parameters": {
"jsCode": "// n8n Code node — Merge originals with dedup clusters and tag each original\n// Upstream nodes:\n// - \"Cleans AI Response\" -> contains ORIGINAL parsed items (A, B, C)\n// - Input to this node -> contains cluster objects from \"Internal Check Duplicate Invoice\"\n// Output: one item per original, with dedup_* fields added\n\n// ---- helpers ----\nfunction canonVendor(v) {\n return String(v || '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ') // strip punctuation (ASCII-safe)\n .replace(/\\b(pt|tbk|inc|ltd|llc)\\b/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\nfunction canonInvoiceNo(inv) {\n return String(inv || '')\n .toUpperCase()\n .replace(/\\s+/g, '')\n .replace(/[^A-Z0-9\\/\\-.]/g, '');\n}\nfunction clusterKeyFromItem(it) {\n const vendor = canonVendor(it.vendor);\n const inv = canonInvoiceNo(it.invoice_no);\n if (inv) return `INV#${vendor}|${inv}`;\n const currency = String(it.currency || '').toUpperCase();\n const amt = Number(it.amount_total || it.subtotal || 0) || 0;\n const date = String(it.due_date || it.issue_date || '');\n return `FALLBACK#${vendor}|${currency}|${amt}|${date}`;\n}\n// Stable-ish signature to match items across steps\nfunction sig(o) {\n return [\n o.source_file_id || '',\n o.source_file_url || '',\n String(o.vendor || '').trim(),\n String(o.invoice_no || '').trim(),\n String(o.issue_date || '').trim(),\n String(o.due_date || '').trim(),\n String(o.currency || '').trim().toUpperCase(),\n String(Number(o.amount_total ?? o.subtotal ?? 0) || 0)\n ].join('||');\n}\n\n// ---- 1) load originals (A, B, C) ----\nconst originals = $('Cleans AI Response').all().map(i => i.json);\n\n// ---- 2) load and normalize clusters from THIS node input ----\nconst rawIn = $input.all().map(i => i.json);\nconst clusters = [];\nfor (const x of rawIn) {\n if (Array.isArray(x)) {\n for (const c of x) if (c && c.cluster_key) clusters.push(c);\n } else if (x && x.cluster_key) {\n clusters.push(x);\n }\n}\n// If your duplicate checker sometimes emits {clusters:[...]}, support that too\nfor (const x of rawIn) {\n if (x && Array.isArray(x.clusters)) {\n for (const c of x.clusters) if (c && c.cluster_key) clusters.push(c);\n }\n}\n\n// ---- 3) index clusters (by cluster_key) ----\nconst clusterIndex = new Map(); // cluster_key -> { meta, keepSig, dropSigSet, keepUsed }\nfor (const c of clusters) {\n const meta = {\n cluster_key: c.cluster_key,\n reason: c.reason || '',\n count: Number(c.count || (c.entries ? c.entries.length : 1)) || 1,\n amounts: c.amounts || [],\n currencies: c.currencies || []\n };\n const keepSig = c.keep ? sig(c.keep) : null;\n const dropSigSet = new Set((c.drop || []).map(sig));\n clusterIndex.set(c.cluster_key, { meta, keepSig, dropSigSet, keepUsed: false });\n}\n\n// Try to find matching cluster for an original item\nfunction findClusterForOriginal(item) {\n const s = sig(item);\n for (const [ck, rec] of clusterIndex) {\n if ((rec.keepSig && rec.keepSig === s) || rec.dropSigSet.has(s)) return [ck, rec];\n }\n const ck = clusterKeyFromItem(item);\n return [ck, clusterIndex.get(ck) || null];\n}\n\n// ---- 4) merge labels onto originals ----\nconst out = [];\n\nfor (const item of originals) {\n const [ck, rec] = findClusterForOriginal(item);\n\n if (!rec) {\n out.push({\n json: {\n ...item,\n dedup_cluster_key: ck || '',\n dedup_role: 'unique',\n dedup_count: 1,\n dedup_reason: 'no_cluster_match'\n }\n });\n continue;\n }\n\n const { meta, keepSig, dropSigSet } = rec;\n const s = sig(item);\n let role;\n\n if (meta.count <= 1) {\n role = 'unique';\n } else if (keepSig && s === keepSig && !rec.keepUsed) {\n role = 'keep'; // first original that matches the keepSig\n rec.keepUsed = true; // ensure only one \"keep\" per cluster\n } else {\n role = 'drop'; // everything else in that cluster is a duplicate\n }\n\n out.push({\n json: {\n ...item,\n dedup_cluster_key: meta.cluster_key,\n dedup_role: role, // \"keep\" | \"drop\" | \"unique\"\n dedup_count: meta.count,\n dedup_reason: meta.reason,\n dedup_amounts: meta.amounts,\n dedup_currencies: meta.currencies\n }\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "123fc9c7-f6d3-4be5-b02a-d6c65faa5bc9",
"name": "병합 Data Value into One Key",
"type": "n8n-nodes-base.code",
"position": [
-4464,
1888
],
"parameters": {
"jsCode": "// Code node — Run once for all items\n// Preceded by a Merge (Wait for all) so ALL items from both paths arrive here.\n\nconst out = [];\n\nfunction get(v) {\n return typeof v === 'string' ? v.trim() : '';\n}\n\nfor (const it of $input.all()) {\n const j = it.json || {};\n\n // Your two possible sources per item\n const fromPdf = get(j.mergedParsedText); // Error3 (PDF path)\n const fromImage = get(j?.ParsedResults?.[0]?.ParsedText); // Error2 (image path)\n\n const OCRResult = fromPdf || fromImage || '';\n const OCRSource = fromPdf\n ? 'Error3.mergedParsedText'\n : fromImage\n ? 'Error2.ParsedResults[0].ParsedText'\n : 'none';\n\n // Keep any file metadata you already have on the item\n out.push({\n json: {\n ...j,\n OCRResult,\n OCRSource\n }\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "88aae252-885b-4433-af13-88fea42ad24e",
"name": "Decide Fate for Data",
"type": "n8n-nodes-base.if",
"position": [
-3216,
1888
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "9cb0bdab-37f9-4536-8b75-de7e3bdd4a76",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.dedup_role }}",
"rightValue": "drop"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "c3b08864-40e6-4c71-b0ad-f46231351a92",
"name": "Create Query for DB",
"type": "n8n-nodes-base.code",
"position": [
-2992,
1312
],
"parameters": {
"jsCode": "// Run ONCE FOR ALL ITEMS\nconst out = [];\n\nfunction windowDates(anchor) {\n if (!anchor) return null;\n const d = new Date(anchor + \"T00:00:00Z\");\n if (isNaN(d)) return null;\n const s = new Date(d); s.setUTCDate(s.getUTCDate() - 30);\n const e = new Date(d); e.setUTCDate(e.getUTCDate() + 30);\n return [s.toISOString().slice(0,10), e.toISOString().slice(0,10)];\n}\n\nfor (const { json: j } of $input.all()) {\n const haveInvoiceNo = !!j?.invoice_no;\n\n // Anchor date:\n // - If we know the invoice number, anchor to invoice-level dates\n // - Otherwise, paid_date is acceptable for finding the invoice by heuristics\n const anchor = haveInvoiceNo\n ? (j?.issue_date || j?.due_date || \"\")\n : (j?.receipt_hints?.paid_date || j?.issue_date || j?.due_date || \"\");\n const dates = windowDates(anchor);\n\n const and = [];\n\n if (haveInvoiceNo) {\n // Prefer canon properties if your DB has them; else fall back to title field\n if (j.invoice_no_canon) {\n and.push({ property: \"Invoice No (Canon)\", rich_text: { equals: String(j.invoice_no_canon) } });\n if (j.vendor_canon) {\n and.push({ property: \"Vendor (Canon)\", rich_text: { equals: String(j.vendor_canon) } });\n }\n } else {\n and.push({ property: \"Invoice No\", title: { equals: String(j.invoice_no) } });\n }\n if (j.currency) {\n and.push({ property: \"Currency\", select: { equals: String(j.currency) } });\n }\n\n // IMPORTANT: no Amount Total filter when Invoice No is present\n // (If you want a belt-and-suspenders check, you can add a WIDE band on amount_total instead,\n // but never use the receipt paid amount.)\n\n } else {\n // Fallback: vendor + currency + invoice total band + date\n if (j.vendor_canon) {\n and.push({ property: \"Vendor (Canon)\", rich_text: { equals: String(j.vendor_canon) } });\n } else if (j.vendor) {\n and.push({ property: \"Vendor\", rich_text: { equals: String(j.vendor) } });\n }\n if (j.currency) {\n and.push({ property: \"Currency\", select: { equals: String(j.currency) } });\n }\n const total = Number(j?.amount_total);\n if (!Number.isNaN(total) && total > 0) {\n const eps = Math.max(5, total * 0.03); // ± $5 or 3%\n and.push(\n { property: \"Amount Total\", number: { greater_than_or_equal_to: +(total - eps).toFixed(2) } },\n { property: \"Amount Total\", number: { less_than_or_equal_to: +(total + eps).toFixed(2) } },\n );\n }\n }\n\n if (dates) {\n const [start, end] = dates;\n and.push({\n or: [\n { property: \"Issue Date\", date: { on_or_after: start, on_or_before: end } },\n { property: \"Due Date\", date: { on_or_after: start, on_or_before: end } },\n ]\n });\n }\n\n const queryBody = { filter: { and }, page_size: haveInvoiceNo ? 10 : 5 };\n out.push({ json: { ...j, queryBody } });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "de2b4a0f-eaba-4d8c-ac76-220f14725f10",
"name": "Check DB Invoice",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2768,
1312
],
"parameters": {
"url": "https://api.notion.com/v1/databases/24df7557957380739611da8371404254/query",
"method": "POST",
"options": {},
"jsonBody": "={{ $json.queryBody }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Notion-Version",
"value": "2022-06-28"
}
]
},
"nodeCredentialType": "notionApi"
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 4.2
},
{
"id": "067db3c6-fc83-46a2-8ad4-0b8cec9ab83e",
"name": "병합 Items Invoice",
"type": "n8n-nodes-base.code",
"position": [
-2544,
1312
],
"parameters": {
"jsCode": "// After \"Check DB Invoice\" (HTTP). Mode: Run once for all items. Single output.\n\n// Originals aligned by index (from your query-prep node)\nconst originals = $('Create Query for DB').all().map(i => i.json);\n\n// Toggle if you want partial flows to auto-route (true) or go to manual_review (false)\nconst ENABLE_PARTIAL = true;\n\n// ---------- Notion helpers ----------\nconst getTitle = (p, name) => (p?.properties?.[name]?.title || [])\n .map(t => t.plain_text || '').join('');\nconst getRich = (p, name) => (p?.properties?.[name]?.rich_text || [])\n .map(t => t.plain_text || '').join('');\nconst getSelect = (p, name) => p?.properties?.[name]?.select?.name || '';\nconst getNumber = (p, name) => (\n typeof p?.properties?.[name]?.number === 'number'\n ? p.properties[name].number\n : null\n);\nconst getDate = (p, name) => p?.properties?.[name]?.date?.start || '';\n\nconst isPaidName = (s) => /^paid/i.test(String(s || ''));\n\n// quick equality for “no-change/dup” check\nfunction coreSame(src, db) {\n const eq = (x,y)=> String(x||'') === String(y||'');\n const numEq=(x,y)=> {\n const nx=Number(x??NaN), ny=Number(y??NaN);\n if (Number.isNaN(nx) && Number.isNaN(ny)) return true;\n return Math.abs(nx - ny) < 0.001;\n };\n return (\n eq(src.vendor, db.vendor) &&\n eq(src.invoice_no, db.invoice_no) &&\n eq(src.currency, db.currency) &&\n numEq(src.amount_total, db.amount_total) &&\n eq(src.issue_date, db.issue_date) &&\n eq(src.due_date, db.due_date)\n );\n}\n\nconst out = [];\nconst tolAbs = 0.01; // absolute amount tolerance\nconst tolPct = 0.01; // 1% relative tolerance\n\n$input.all().forEach((it, idx) => {\n const src = originals[idx] || {};\n const http = it.json || {};\n const pages = Array.isArray(http.results) ? http.results : [];\n const manyMatches = pages.length > 1;\n const page = pages[0] || null;\n\n // ---------- DB snapshot ----------\n const db = page ? {\n page_id: page.id,\n invoice_no: getTitle(page, 'Invoice No'),\n vendor: getRich(page, 'Vendor'),\n currency: getSelect(page,'Currency'),\n amount_total:getNumber(page,'Amount Total'),\n issue_date: getDate(page, 'Issue Date'),\n due_date: getDate(page, 'Due Date'),\n status: getSelect(page,'Status'),\n } : null;\n const dbPaid = db ? isPaidName(db.status) : false;\n\n // ---------- mismatches ----------\n const vendorMismatch = !!(db && db.vendor && src.vendor && db.vendor !== src.vendor);\n const currencyMismatch = !!(db && db.currency && src.currency && db.currency !== src.currency);\n\n const inAmt = Number(src.amount_total ?? NaN);\n const dbAmt = Number(db?.amount_total ?? NaN);\n const amountMismatch =\n db && Number.isFinite(inAmt) && Number.isFinite(dbAmt) &&\n (Math.abs(inAmt - dbAmt) > tolAbs) &&\n (Math.abs(inAmt - dbAmt) / Math.max(1, dbAmt) > tolPct);\n\n const fallbackAmbiguous = !src.invoice_no && manyMatches;\n\n // ---------- derive payment signals from AMOUNTS (ignore labels) ----------\n const statusStr = String(src.status || '');\n const totalIn = Number(src.amount_total ?? NaN);\n const paidAmtIn = Number(\n (src?.receipt_hints?.paid_amount ?? src.paid_amount) ?? NaN\n );\n const dueIn = Number(src.amount_due ?? NaN);\n\n // some payment if: positive paid OR “partial” in status OR doc_says_paid flag\n let hasSomePayment =\n (Number.isFinite(paidAmtIn) && paidAmtIn > tolAbs) ||\n /partial/i.test(statusStr) ||\n src.doc_says_paid === true;\n\n // full payment primarily by amounts; labels only if amounts missing\n let isFullPayment =\n (Number.isFinite(dueIn) && dueIn <= tolAbs) ||\n (Number.isFinite(totalIn) && Number.isFinite(paidAmtIn) && (paidAmtIn >= totalIn - tolAbs));\n\n // if due is positive, it cannot be full—even if the doc says “PAID”\n if (Number.isFinite(dueIn) && dueIn > tolAbs) isFullPayment = false;\n\n // keep compatibility names used later in logic\n const incomingPaid = hasSomePayment;\n const partialDetected = hasSomePayment && !isFullPayment;\n\n // ---------- decide route ----------\n let next_action = 'archive';\n let reason = '';\n\n if (!page) {\n // No DB page found → choose create_* route\n if (hasSomePayment) {\n if (isFullPayment) {\n next_action = 'create_paid';\n reason = 'no_db_match_paid';\n } else {\n next_action = ENABLE_PARTIAL ? 'create_partial' : 'manual_review';\n reason = ENABLE_PARTIAL ? 'no_db_match_partial_payment' : 'partial_needs_review';\n }\n } else {\n next_action = 'create_unpaid';\n reason = 'no_db_match_unpaid';\n }\n } else {\n // Have a DB page\n if (manyMatches || vendorMismatch || currencyMismatch || fallbackAmbiguous) {\n next_action = 'manual_review';\n reason = manyMatches\n ? 'multiple_db_matches'\n : vendorMismatch\n ? 'vendor_mismatch'\n : currencyMismatch\n ? 'currency_mismatch'\n : 'fallback_ambiguous';\n } else if (hasSomePayment) {\n // There is some payment in the incoming doc\n if (isFullPayment) {\n // mark paid if DB not yet paid; else archive duplicate payment\n if (!dbPaid) {\n next_action = 'update_mark_paid';\n reason = 'mark_paid';\n } else {\n next_action = 'archive';\n reason = 'duplicate_payment_already_paid';\n }\n } else {\n // partial payment path\n if (ENABLE_PARTIAL) {\n next_action = 'update_partial';\n reason = 'partial_payment_detected';\n } else {\n next_action = 'manual_review';\n reason = 'partial_needs_review';\n }\n }\n } else if (amountMismatch) {\n next_action = 'manual_review';\n reason = 'amount_mismatch';\n } else {\n // no payment transition\n const same = coreSame(src, db);\n next_action = 'archive';\n reason = same ? 'duplicate_no_change' : 'no_payment_transition';\n }\n }\n\n out.push({\n json: {\n ...src,\n db_found: !!page,\n db_page_id: db?.page_id || '',\n db_status: db?.status || '',\n next_action,\n reason,\n _checks: {\n manyMatches,\n vendorMismatch,\n currencyMismatch,\n amountMismatch,\n fallbackAmbiguous,\n hasSomePayment,\n isFullPayment,\n dbPaid\n }\n }\n });\n});\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "1e7efdbb-f58c-4cfa-9a74-22cda80b58c7",
"name": "Send to Source File Invoice 1",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
736
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "3b8e7c65-8d40-4636-bfb0-8091365e584e",
"name": "Prepare Line Items before send",
"type": "n8n-nodes-base.code",
"position": [
-2320,
1312
],
"parameters": {
"jsCode": "// n8n Code node — prepare line items + compute ONE field for Notion (\"Paid Amount\")\nconst out = [];\n\nfunction two(n){ return Math.round((Number(n)||0)*100)/100; }\nfunction chunk(str, size=1800){ const a=[]; for(let i=0;i<str.length;i+=size)a.push(str.slice(i,i+size)); return a; }\nfunction num(x){ if(x===null||x===undefined||x===\"\") return NaN; const n=Number(String(x).replace(/[^0-9.-]/g,\"\")); return isNaN(n)?NaN:n; }\nfunction clamp(n,min,max){ return Math.max(min, Math.min(max, n)); }\nfunction approxEq(a,b,eps=0.01){ return Math.abs(a-b)<=eps; }\n\nfor (const item of $input.all()) {\n const j = item.json || {};\n const ccy = (j.currency || '').toUpperCase();\n const items = Array.isArray(j.line_items) ? j.line_items : [];\n\n // Build line-items text (unchanged)\n const lines = items.map(li => {\n const desc=(li?.description||'').trim();\n const qty = two(li?.qty);\n const up = two(li?.unit_price);\n const amt = two(li?.amount || qty*up);\n const tail = ccy ? ` ${ccy}` : '';\n return `${qty} × ${desc} @ ${up}${tail} = ${amt}${tail}`;\n });\n const text = lines.join('\\n') || '';\n const rich = chunk(text).map(s => ({ type:'text', text:{ content:s }}));\n\n // Minimal compute for Paid Amount\n const total = num(j.amount_total);\n const due = num(j.amount_due);\n const delta = num(j?.receipt_hints?.paid_amount ?? j.paid_amount); // new receipt (e.g., 200)\n const prev = num(j.db_paid_amount_prev ?? j.invoice?.paid_amount ?? 0); // previous paid if you pass it\n\n // Prefer set-to from doc (Total - Due); fallback to prev + delta\n let newPaid = Number.isFinite(total) && Number.isFinite(due)\n ? two(clamp(total - due, 0, total))\n : (Number.isFinite(delta) ? two(clamp(prev + delta, 0, Number.isFinite(total)? total : Infinity)) : NaN);\n\n // If still NaN, don't emit the field to avoid bad writes\n const outJson = {\n ...j,\n line_items_text: text,\n line_items_rich_text: rich,\n };\n if (Number.isFinite(newPaid)) outJson.notion_paid_amount = newPaid;\n\n // (Optional tiny extra: status, only if you want it — safe to ignore)\n if (Number.isFinite(total) && Number.isFinite(newPaid)) {\n outJson.notion_status =\n approxEq(newPaid, 0) ? \"Unpaid\" :\n approxEq(newPaid, total) ? \"Paid (Verified)\" :\n \"Partially Paid\";\n }\n\n out.push({ json: outJson });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "fa73399a-c133-48b3-87fe-922961f4d448",
"name": "Decide Fate",
"type": "n8n-nodes-base.switch",
"position": [
-2096,
1232
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Create Unpaid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "18f4cc4b-ad0f-4782-a59d-a66dbffc6475",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "create_unpaid"
}
]
},
"renameOutput": true
},
{
"outputKey": "Create Paid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d8bb0418-0798-428a-b379-8cf8accf5ea9",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "create_paid"
}
]
},
"renameOutput": true
},
{
"outputKey": "Update to Paid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4009916c-000e-435d-ada3-6a7d1fbc595c",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "update_mark_paid"
}
]
},
"renameOutput": true
},
{
"outputKey": "Archive",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "172dad2a-7a92-493d-977d-56c568d5c360",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "archive"
}
]
},
"renameOutput": true
},
{
"outputKey": "New Create Partial",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "a14d6d5c-6eac-49e8-8380-f85cf565a542",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "create_partial"
}
]
},
"renameOutput": true
},
{
"outputKey": "Update Partial Payment",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b9d0f364-e7a4-48b8-94fc-6053b51f6e14",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "update_partial"
}
]
},
"renameOutput": true
},
{
"outputKey": "Manual Review",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4d8986d1-3896-42bf-9d6f-9adb48ac3987",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "manual_review"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "f90f09ea-6016-429b-915f-1565bcff1d97",
"name": "Create new Invoice Unpaid",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
736
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Decide Fate').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Decide Fate').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Decide Fate').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Decide Fate').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Decide Fate').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Decide Fate').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Decide Fate').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Decide Fate').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Decide Fate').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Decide Fate').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Decide Fate').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Decide Fate').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Decide Fate').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Decide Fate').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "8f25494a-6b7c-45e5-99fc-7ab5e2e0d085",
"name": "Add Receipt into Cashflow",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
1312
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $json.invoice_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.db_page_id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $json.payment_method }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.line_items_rich_text[0].text.content }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "35db92f5-faf6-4ecd-a148-7bbf09bf620b",
"name": "Update Invoice to Paid Fully",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
1312
],
"parameters": {
"pageId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Prepare Line Items before send').item.json.db_page_id }}"
},
"options": {},
"resource": "databasePage",
"operation": "update",
"propertiesUi": {
"propertyValues": [
{
"key": "Receipt|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Status|select",
"selectValue": "Paid"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "5487835c-5655-476b-bbad-f368653150d1",
"name": "Send to Source File Invoice 2",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
928
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "a09de3d7-f79a-4027-ab49-a4e2376d3f82",
"name": "Add Receipt into Cashflow Paid 1",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
928
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $('Prepare Line Items before send').item.json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.receipt_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "54b5f427-4c28-4b83-a04d-40bc14df309d",
"name": "Create new Invoice Paid Full",
"type": "n8n-nodes-base.notion",
"position": [
-1424,
928
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Decide Fate').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Decide Fate').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Decide Fate').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Decide Fate').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Decide Fate').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Decide Fate').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Decide Fate').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Decide Fate').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Decide Fate').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Decide Fate').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Decide Fate').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Decide Fate').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Decide Fate').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Decide Fate').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $('Send to Source File Invoice 2').item.json.id }}"
]
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "48bc9ab1-03bb-40a6-844d-a4fff203fcd5",
"name": "Send to Archive Source File Invoice",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
1120
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "239f7557-9573-80d6-a8fb-fc710833e5db",
"cachedResultUrl": "https://www.notion.so/239f7557957380d6a8fbfc710833e5db",
"cachedResultName": "Archived Source File (Duplicate)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "FILE URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "d165164c-bcf8-4ef3-b434-9416865c6b26",
"name": "Archive Invoice",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
1120
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "254f7557-9573-8012-b9b3-e7811bfac12c",
"cachedResultUrl": "https://www.notion.so/254f755795738012b9b3e7811bfac12c",
"cachedResultName": "Archived Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Decide Fate').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Decide Fate').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Decide Fate').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Decide Fate').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Decide Fate').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Decide Fate').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Decide Fate').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Decide Fate').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Decide Fate').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Decide Fate').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Decide Fate').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Decide Fate').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Decide Fate').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Decide Fate').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "4157135a-0645-4f12-9315-c6b2299520b6",
"name": "대기",
"type": "n8n-nodes-base.wait",
"position": [
-1600,
2544
],
"webhookId": "9d0e00b6-5ee8-45c9-8597-34db73f701ae",
"parameters": {
"resume": "webhook",
"options": {},
"resumeUnit": "days",
"resumeAmount": 2,
"limitWaitTime": true
},
"typeVersion": 1.1
},
{
"id": "da9cdafe-01c8-49c8-afaa-29cb46f67aef",
"name": "Send a message",
"type": "n8n-nodes-base.slack",
"position": [
-1824,
2544
],
"webhookId": "2df2d5da-990f-4499-9131-0bbf3595e9bc",
"parameters": {
"text": "Test Text",
"select": "channel",
"blocksUi": "={\n\t\"blocks\": [\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": \"You have a new invoice to review:\\n*{{ $('Prepare Line Items before send').item.json.invoice_no || '—' }}*\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"fields\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Type:*\\nInvoice\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*When:*\\n{{ $('Prepare Line Items before send').item.json.issue_date || '—' }} → {{ $('Prepare Line Items before send').item.json.due_date || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Vendor:*\\n{{ $('Prepare Line Items before send').item.json.vendor || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Currency:*\\n{{ $('Prepare Line Items before send').item.json.currency || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Invoice No:*\\n{{ $('Prepare Line Items before send').item.json.invoice_no || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Status (detected):*\\n{{ $('Prepare Line Items before send').item.json.status || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Amount Total:*\\n{{ $('Prepare Line Items before send').item.json.amount_total || 0 }} {{ $('Prepare Line Items before send').item.json.currency || '' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Paid / Due:*\\n{{ $('Prepare Line Items before send').item.json.paid_amount || 0 }} / {{ $('Prepare Line Items before send').item.json.amount_due || 0 }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Payment Method:*\\n{{ $('Prepare Line Items before send').item.json.payment_method || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Reason:*\\n{{ $('Prepare Line Items before send').item.json.reason || '—' }}\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"type\": \"image\",\n\t\t\t\"title\": {\n\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\"text\": \"image1\",\n\t\t\t\t\"emoji\": true\n\t\t\t},\n\t\t\t\"image_url\": \"{{ $json.source_file_url }}\",\n\t\t\t\"alt_text\": \"image1\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"divider\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"actions\",\n\t\t\t\"block_id\": \"review_actions\",\n\t\t\t\"elements\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"action_id\": \"approve\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"text\": \"✅ Approve Paid\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": \"primary\",\n\t\t\t\t\t\"url\": \"{{ $execution.resumeUrl }}?choice=approve&inv={{ encodeURIComponent($('Prepare Line Items before send').item.json.invoice_no || '') }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"action_id\": \"unpaid\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"text\": \"📝 Approve Unpaid\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": \"danger\",\n\t\t\t\t\t\"url\": \"{{ $execution.resumeUrl }}?choice=unpaid&inv={{ encodeURIComponent($('Prepare Line Items before send').item.json.invoice_no || '') }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"action_id\": \"archive\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"text\": \"❌ Archive\"\n\t\t\t\t\t},\n\t\t\t\t\t\"url\": \"{{ $execution.resumeUrl }}?choice=archive&inv={{ encodeURIComponent($('Prepare Line Items before send').item.json.invoice_no || '') }}\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09B5P66Q4A",
"cachedResultName": "required-invoice-review"
},
"messageType": "block",
"otherOptions": {}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "e2179126-0881-4701-9d4a-62d335a74abd",
"name": "Send to Archive Source File Invoice Duplicate",
"type": "n8n-nodes-base.notion",
"position": [
-2768,
2032
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "239f7557-9573-80d6-a8fb-fc710833e5db",
"cachedResultUrl": "https://www.notion.so/239f7557957380d6a8fbfc710833e5db",
"cachedResultName": "Archived Source File (Duplicate)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "FILE URL|url",
"urlValue": "={{ $json.source_file_url }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "cca27d76-da70-42fc-b6b2-5c5330490d2b",
"name": "Archive Invoice Duplicate",
"type": "n8n-nodes-base.notion",
"position": [
-2544,
2032
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "254f7557-9573-8012-b9b3-e7811bfac12c",
"cachedResultUrl": "https://www.notion.so/254f755795738012b9b3e7811bfac12c",
"cachedResultName": "Archived Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Prepare Archive Duplicate Items').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Prepare Archive Duplicate Items').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Prepare Archive Duplicate Items').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Prepare Archive Duplicate Items').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.line_items_text }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Prepare Archive Duplicate Items').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "e4d849f2-4c10-4e93-9133-b78e861687f2",
"name": "Prepare Archive Duplicate Items",
"type": "n8n-nodes-base.code",
"position": [
-2992,
2032
],
"parameters": {
"jsCode": "// Build \"summary items\" text from line_items (one output per input)\n\nfunction toNum(v) {\n const n = Number(v);\n return Number.isFinite(n) ? n : 0;\n}\nfunction clean(s) {\n return String(s ?? \"\").trim();\n}\nfunction fmt(n) {\n // 2dp but strip trailing .00 if integer\n const f = (Math.round(n * 100) / 100).toFixed(2);\n return f.endsWith(\".00\") ? String(Math.round(n)) : f;\n}\n\nconst out = [];\n\nfor (const { json: rec } of $input.all()) {\n const currency = clean(rec.currency).toUpperCase(); // e.g., \"USD\"\n const lis = Array.isArray(rec.line_items) ? rec.line_items : [];\n\n // Group by description + unit_price to collapse duplicates\n const groups = new Map();\n for (const li of lis) {\n const desc = clean(li?.description) || \"Item\";\n const unit = toNum(li?.unit_price);\n const qty = toNum(li?.qty);\n const amt = toNum(li?.amount) || qty * unit;\n\n const key = `${desc}||${unit}`;\n if (!groups.has(key)) groups.set(key, { desc, unit, qty: 0, amount: 0 });\n const g = groups.get(key);\n g.qty += qty;\n g.amount += amt;\n }\n\n // Render lines\n const lines = [];\n const rich = [];\n for (const g of groups.values()) {\n const qtyS = fmt(g.qty);\n const unitS = fmt(g.unit);\n const amtS = fmt(g.amount);\n const curS = currency ? ` ${currency}` : \"\";\n const line = `${qtyS} × ${g.desc} @ ${unitS}${curS} = ${amtS}${curS}`;\n lines.push(line);\n rich.push({ type: \"text\", text: { content: line } });\n }\n\n const summary = lines.join(\"\\n\");\n\n out.push({\n json: {\n ...rec,\n summary_items: summary, // <-- your \"Summary Items\" field\n line_items_text: summary, // keep old name if you need it\n line_items_rich_text: rich // Notion rich_text-friendly\n }\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "237841d5-2268-4efe-9ae2-25e904dbd48a",
"name": "Send to Source File Invoice 3",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2288
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "010957b9-c7a4-4432-ac02-6ea1400f0418",
"name": "Add Receipt into Cashflow Paid 2",
"type": "n8n-nodes-base.notion",
"position": [
-480,
2288
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $('Take Invoice Details 1').item.json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Take Invoice Details 1').item.json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $('Take Invoice Details 1').item.json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.receipt_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "5b13d0d1-28a9-47df-82a2-5b83d338c413",
"name": "Approval Check",
"type": "n8n-nodes-base.switch",
"position": [
-1376,
2528
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Paid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cf7a1e2d-9984-4052-9c1f-691f7a0ea50c",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.query.choice }}",
"rightValue": "approve"
}
]
},
"renameOutput": true
},
{
"outputKey": "Unpaid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "78953ded-6256-4f3a-86c5-1189b02b20e7",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.query.choice }}",
"rightValue": "unpaid"
}
]
},
"renameOutput": true
},
{
"outputKey": "Archive",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "3f329867-45ba-4287-b5f7-632c52567c79",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.query.choice }}",
"rightValue": "archive"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "0163f077-3850-4609-9def-1483cd5f720b",
"name": "Add Receipt into Cashflow1",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2096
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $json.invoice_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.db_page_id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $json.payment_method }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.line_items_rich_text[0].text.content }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "557a172b-f58a-4f82-a8b8-ddc3865bbe20",
"name": "Update Invoice to Paid Fully 1",
"type": "n8n-nodes-base.notion",
"position": [
-480,
2096
],
"parameters": {
"pageId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Take Invoice Details 1').item.json.db_page_id }}"
},
"options": {},
"resource": "databasePage",
"operation": "update",
"propertiesUi": {
"propertyValues": [
{
"key": "Receipt|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
},
{
"key": "Status|select",
"selectValue": "Paid"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "5e962f4f-f35a-46f6-a80f-bdd66553c3ed",
"name": "Create new Invoice Paid 3",
"type": "n8n-nodes-base.notion",
"position": [
-256,
2288
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Take Invoice Details 1').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Take Invoice Details 1').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Take Invoice Details 1').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Take Invoice Details 1').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Take Invoice Details 1').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $('Send to Source File Invoice 3').item.json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "5fcebb58-8279-4d89-981f-27d34f2c2747",
"name": "Take Invoice Details 2",
"type": "n8n-nodes-base.code",
"position": [
-1152,
2576
],
"parameters": {
"jsCode": "// Code node: Pick Reviewed Invoice\n// Input: the Wait node callback (has query.choice, query.inv)\n// Also reads items from \"Prepare Line Items before send\"\n// Output: ONE item = the picked invoice + review info\n\nconst choice = $json?.query?.choice || ''; // approve | unpaid | archive\nconst invRaw = $json?.query?.inv || '';\nconst inv = String(invRaw).trim();\n\nconst prepared = $('Prepare Line Items before send').all().map(i => i.json);\nif (!prepared.length) {\n throw new Error('No items found in \"Prepare Line Items before send\".');\n}\n\n// try exact invoice_no match first\nlet picked = inv\n ? prepared.find(x => String(x.invoice_no || '').trim() === inv)\n : undefined;\n\n// soft fallback(s)\nif (!picked) {\n if (prepared.length === 1) {\n picked = prepared[0];\n } else {\n const canon = s => String(s || '').toLowerCase().replace(/\\s+/g, '').trim();\n const invC = canon(inv);\n picked = prepared.find(x => canon(x.invoice_no) === invC) || prepared[0];\n }\n}\n\n// annotate with the review decision so the next Switch can route\nconst out = {\n ...picked,\n review: {\n choice, // approve | unpaid | archive\n inv, // invoice_no from the button\n source: 'slack_button'\n }\n};\n\nreturn [{ json: out }];\n"
},
"typeVersion": 2
},
{
"id": "d6375c43-4c99-41fe-9215-eec2ba228e17",
"name": "Take Invoice Details 1",
"type": "n8n-nodes-base.code",
"position": [
-1152,
2192
],
"parameters": {
"jsCode": "// Code node: Pick Reviewed Invoice\n// Input: the Wait node callback (has query.choice, query.inv)\n// Also reads items from \"Prepare Line Items before send\"\n// Output: ONE item = the picked invoice + review info\n\nconst choice = $json?.query?.choice || ''; // approve | unpaid | archive\nconst invRaw = $json?.query?.inv || '';\nconst inv = String(invRaw).trim();\n\nconst prepared = $('Prepare Line Items before send').all().map(i => i.json);\nif (!prepared.length) {\n throw new Error('No items found in \"Prepare Line Items before send\".');\n}\n\n// try exact invoice_no match first\nlet picked = inv\n ? prepared.find(x => String(x.invoice_no || '').trim() === inv)\n : undefined;\n\n// soft fallback(s)\nif (!picked) {\n if (prepared.length === 1) {\n picked = prepared[0];\n } else {\n const canon = s => String(s || '').toLowerCase().replace(/\\s+/g, '').trim();\n const invC = canon(inv);\n picked = prepared.find(x => canon(x.invoice_no) === invC) || prepared[0];\n }\n}\n\n// annotate with the review decision so the next Switch can route\nconst out = {\n ...picked,\n review: {\n choice, // approve | unpaid | archive\n inv, // invoice_no from the button\n source: 'slack_button'\n }\n};\n\nreturn [{ json: out }];\n"
},
"typeVersion": 2
},
{
"id": "a9a19c40-4875-4c02-9c2b-3f65124ebdec",
"name": "Update Invoice to Paid Fully ",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2480
],
"parameters": {
"pageId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Take Invoice Details 2').item.json.db_page_id }}"
},
"options": {},
"resource": "databasePage",
"operation": "update",
"propertiesUi": {
"propertyValues": [
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.paid_amount }}"
},
{
"key": "Status|select",
"selectValue": "=Unpaid"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "0af156a4-3324-4a1f-9f63-7a181adb4485",
"name": "Send to Source File Invoice ",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2672
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "a1eaf719-3843-424f-b758-b58b32cdc30c",
"name": "Create new Invoice Paid ",
"type": "n8n-nodes-base.notion",
"position": [
-480,
2672
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Take Invoice Details 2').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Take Invoice Details 2').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Take Invoice Details 2').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Take Invoice Details 2').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.notes }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Take Invoice Details 2').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $('Send to Source File Invoice ').item.json.id }}"
]
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.paid_amount }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "2d7330e7-e3b9-4afc-9217-9dcbdbd4a3e9",
"name": "Archive Invoice 1",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2864
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "254f7557-9573-8012-b9b3-e7811bfac12c",
"cachedResultUrl": "https://www.notion.so/254f755795738012b9b3e7811bfac12c",
"cachedResultName": "Archived Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Take Invoice Details 3').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Take Invoice Details 3').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Take Invoice Details 3').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Take Invoice Details 3').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Take Invoice Details 3').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.notes || $('Take Invoice Details 3').item.json.reason}}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 3').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Take Invoice Details 3').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Take Invoice Details 3').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Take Invoice Details 3').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "6342d55e-4e82-4755-8826-3d4e5b7f9be7",
"name": "Take Invoice Details 3",
"type": "n8n-nodes-base.code",
"position": [
-1152,
2864
],
"parameters": {
"jsCode": "// Code node: Pick Reviewed Invoice\n// Input: the Wait node callback (has query.choice, query.inv)\n// Also reads items from \"Prepare Line Items before send\"\n// Output: ONE item = the picked invoice + review info\n\nconst choice = $json?.query?.choice || ''; // approve | unpaid | archive\nconst invRaw = $json?.query?.inv || '';\nconst inv = String(invRaw).trim();\n\nconst prepared = $('Prepare Line Items before send').all().map(i => i.json);\nif (!prepared.length) {\n throw new Error('No items found in \"Prepare Line Items before send\".');\n}\n\n// try exact invoice_no match first\nlet picked = inv\n ? prepared.find(x => String(x.invoice_no || '').trim() === inv)\n : undefined;\n\n// soft fallback(s)\nif (!picked) {\n if (prepared.length === 1) {\n picked = prepared[0];\n } else {\n const canon = s => String(s || '').toLowerCase().replace(/\\s+/g, '').trim();\n const invC = canon(inv);\n picked = prepared.find(x => canon(x.invoice_no) === invC) || prepared[0];\n }\n}\n\n// annotate with the review decision so the next Switch can route\nconst out = {\n ...picked,\n review: {\n choice, // approve | unpaid | archive\n inv, // invoice_no from the button\n source: 'slack_button'\n }\n};\n\nreturn [{ json: out }];\n"
},
"typeVersion": 2
},
{
"id": "c6f37078-96cb-4e97-b678-ebb688afb413",
"name": "Check DB Exist 1",
"type": "n8n-nodes-base.if",
"position": [
-928,
2192
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "95b89f55-ca6f-46ce-bc0c-8ea48b18c013",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.db_found }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "2b1a1c57-bad7-4629-a969-aced98962828",
"name": "Check DB Exist 2",
"type": "n8n-nodes-base.if",
"position": [
-928,
2576
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "95b89f55-ca6f-46ce-bc0c-8ea48b18c013",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.db_found }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "ed421b2f-4a7e-4098-9d09-96965813f3e4",
"name": "Send to Source File Invoice 4",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
1504
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "3f33a49f-f4d5-4483-b01b-ab7b05f1edef",
"name": "Add Receipt into Cashflow Paid Partially",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
1504
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $('Prepare Line Items before send').item.json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.notes || \"Partial Paid\" }}"
},
{
"key": "Timestamp|date",
"date": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.receipt_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "a988e2c7-ad51-4df3-b4d6-9d5cd6e123af",
"name": "Create new Invoice Paid Partially",
"type": "n8n-nodes-base.notion",
"position": [
-1424,
1504
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Prepare Line Items before send').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Prepare Line Items before send').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Prepare Line Items before send').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $('Send to Source File Invoice 4').item.json.id }}"
]
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "8e8ac821-5cd5-4bd2-b78d-203fd5cf57fb",
"name": "Add Receipt into Cashflow2",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
1696
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $json.invoice_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.db_page_id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $json.payment_method }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.line_items_rich_text[0].text.content }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "25951d6e-c27d-48b0-8bef-1b06bb931c30",
"name": "Update Invoice to Paid Partial or Fully",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
1696
],
"parameters": {
"pageId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Prepare Line Items before send').item.json.db_page_id }}"
},
"options": {},
"resource": "databasePage",
"operation": "update",
"propertiesUi": {
"propertyValues": [
{
"key": "Receipt|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.notion_paid_amount }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.notion_status }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "1ed7bced-88d4-4210-b323-c64bf1c1fece",
"name": "Check parsing Error2",
"type": "n8n-nodes-base.if",
"position": [
-4688,
1552
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "ddbbbfbb-e05b-4ca9-b607-65ad8ac748bb",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.IsErroredOnProcessing }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "1c81ef35-2d9b-435b-90ae-979dbebe8ace",
"name": "OCR Space Parse",
"type": "n8n-nodes-base.httpRequest",
"position": [
-4912,
1552
],
"parameters": {
"url": "https://api.ocr.space/parse/image",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "filetype",
"value": "jpg"
},
{
"name": "file",
"parameterType": "formBinaryData",
"inputDataFieldName": "data"
},
{
"name": "scale",
"value": "true"
},
{
"name": "OCREngine",
"value": "2"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"id": "dkJjl1msBNIeIT5u",
"name": "OCR Space"
}
},
"retryOnFail": true,
"typeVersion": 4.2
},
{
"id": "7dde97e8-b6cd-4527-a948-36d62ca9d235",
"name": "Take Binary Files",
"type": "n8n-nodes-base.httpRequest",
"position": [
-5136,
1552
],
"parameters": {
"url": "={{ $json.url_private_download }}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "slackApi"
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 4.2
},
{
"id": "d2da0e22-0a2f-4a66-8651-7a0bd73f3452",
"name": "중지 및 오류",
"type": "n8n-nodes-base.stopAndError",
"position": [
-4464,
1504
],
"parameters": {
"errorMessage": "=OCR Server Parsing Failed Reason:{{ $json.ErrorMessage[0] }}"
},
"typeVersion": 1
},
{
"id": "6376b9b6-40c7-4f01-8a11-8f5bee34dfd4",
"name": "중지 및 오류1",
"type": "n8n-nodes-base.stopAndError",
"position": [
-4688,
1744
],
"parameters": {
"errorMessage": "=OCR Server Parsing Failed Reason:{{ $json.ErrorMessage[0] }}"
},
"typeVersion": 1
},
{
"id": "17f5f3e0-7aa0-45c2-9c84-d840321ef94a",
"name": "집계1",
"type": "n8n-nodes-base.aggregate",
"position": [
-2320,
2032
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "d707c4fc-d2a5-496d-9d6b-8c71f627ad21",
"name": "Send Duplicate Notification",
"type": "n8n-nodes-base.slack",
"position": [
-2096,
2032
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟥 Internal Duplicate Found:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0971ANKZM1",
"cachedResultName": "duplicate-alert"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "548ea7e3-8e51-4a56-891e-1db51e466b2d",
"name": "집계",
"type": "n8n-nodes-base.aggregate",
"position": [
-1424,
736
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "893731ba-84cd-4189-9c3d-c1a73c158979",
"name": "집계2",
"type": "n8n-nodes-base.aggregate",
"position": [
-1200,
928
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "2d9bf727-1b2f-4a1e-bc54-61c686f63d01",
"name": "Notify New Invoice with Receipt",
"type": "n8n-nodes-base.slack",
"position": [
-976,
928
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟩 Paid Invoice Created:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "cb2e3474-2c42-4804-a1a0-aea65c45aece",
"name": "Notify New Invoice",
"type": "n8n-nodes-base.slack",
"position": [
-1200,
736
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟨 Invoice Created:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "6db4ac23-65d8-4e5a-b1ae-84777f7c6a0b",
"name": "집계3",
"type": "n8n-nodes-base.aggregate",
"position": [
-1424,
1312
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "7766fd88-d419-47ee-b545-e6d6d574045f",
"name": "Notify Update Invoice",
"type": "n8n-nodes-base.slack",
"position": [
-1200,
1312
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟩 Invoice Updated to Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "ec4398cb-c999-43bb-9d71-76a9ec6e2747",
"name": "집계4",
"type": "n8n-nodes-base.aggregate",
"position": [
-1424,
1120
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "8976661d-c12c-4b3b-92c9-337fb7b3be3f",
"name": "집계5",
"type": "n8n-nodes-base.aggregate",
"position": [
-1200,
1504
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "20260742-e05b-4ce1-b2ca-40db32c1e66b",
"name": "집계6",
"type": "n8n-nodes-base.aggregate",
"position": [
-1424,
1696
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "75d16382-b71a-413c-abb7-5f6ee529b268",
"name": "Notify Update Invoice Partial Paid",
"type": "n8n-nodes-base.slack",
"position": [
-1200,
1696
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟡 Updated Invoice Partial Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "be11d35c-a96d-432d-bf60-747e9242b8b0",
"name": "Notify Create Invoice Partial Paid",
"type": "n8n-nodes-base.slack",
"position": [
-976,
1504
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟡 Created New Invoice Partial Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "cb059bc5-42c4-4eee-a4c5-f70013b81d1f",
"name": "Notify Invoice Archived",
"type": "n8n-nodes-base.slack",
"position": [
-1200,
1120
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟥 Database Duplicate Found:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "29cc18f8-253c-4763-a2ae-e9f2668b4e8c",
"name": "집계8",
"type": "n8n-nodes-base.aggregate",
"position": [
-480,
2864
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "852ca201-dfe6-4d5b-bf6d-9d06954f0604",
"name": "Notify Invoice Archived2",
"type": "n8n-nodes-base.slack",
"position": [
-256,
2864
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟥 Manual Review Archived:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "2e9ab2da-f324-4864-8617-4ebf6d6873ab",
"name": "Send to Archive Source File Invoice ",
"type": "n8n-nodes-base.notion",
"position": [
-928,
2864
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "239f7557-9573-80d6-a8fb-fc710833e5db",
"cachedResultUrl": "https://www.notion.so/239f7557957380d6a8fbfc710833e5db",
"cachedResultName": "Archived Source File (Duplicate)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "FILE URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "ff53fb86-f773-4690-b874-be81d77259ab",
"name": "집계7",
"type": "n8n-nodes-base.aggregate",
"position": [
-256,
2672
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "38fd6b37-35cd-43f4-a6a1-caf4c20f0848",
"name": "Notify New Invoice1",
"type": "n8n-nodes-base.slack",
"position": [
-32,
2672
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟨 Manual Review Invoice Created Unpaid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "30d801f2-7ae0-4f43-882d-2df58867102e",
"name": "집계9",
"type": "n8n-nodes-base.aggregate",
"position": [
-480,
2480
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "3fbead5e-2aa0-4222-a2ff-2a03901ffbe0",
"name": "Notify New Invoice2",
"type": "n8n-nodes-base.slack",
"position": [
-256,
2480
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟨 Manual Review Invoice Updated Unpaid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "9706d716-8bfc-433e-9d09-8a3a479a5ecc",
"name": "집계10",
"type": "n8n-nodes-base.aggregate",
"position": [
-32,
2288
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "2731b665-b116-459e-9a90-1d28d63d1c85",
"name": "Notify New Invoice with Receipt1",
"type": "n8n-nodes-base.slack",
"position": [
192,
2288
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟩 Manual Review Invoice Created Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "ffec1ef1-51d6-4870-b4e6-86af6c95ed87",
"name": "집계11",
"type": "n8n-nodes-base.aggregate",
"position": [
-256,
2096
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "b206f8e8-74f5-4502-8dc1-ef154867890f",
"name": "Notify New Invoice with Receipt2",
"type": "n8n-nodes-base.slack",
"position": [
-32,
2096
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟩 Manual Review Invoice Updated Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "b4ea52bb-acc9-42d9-a61b-0f89bcfabba0",
"name": "메모",
"type": "n8n-nodes-base.stickyNote",
"position": [
-6112,
1440
],
"parameters": {
"width": 672,
"height": 704,
"content": "# 🔤 Input & Detect Format"
},
"typeVersion": 1
},
{
"id": "4a5b2a7d-80d5-4897-884c-00d0451daee0",
"name": "메모1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-5376,
1440
],
"parameters": {
"color": 2,
"width": 1056,
"height": 704,
"content": "# 👨💻 Parse Data\n"
},
"typeVersion": 1
},
{
"id": "c3f62378-ecf0-4bd2-88d1-63e5268ef615",
"name": "메모2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4272,
1440
],
"parameters": {
"color": 3,
"width": 1200,
"height": 704,
"content": "# 🤖 AI Format & Detect Duplicate\n"
},
"typeVersion": 1
},
{
"id": "3f9110e8-9778-48e4-ab48-286bdfefc291",
"name": "메모3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3040,
1888
],
"parameters": {
"color": 7,
"width": 1120,
"height": 384,
"content": "# ❌ Archive Duplicates\n"
},
"typeVersion": 1
},
{
"id": "f32354a7-d05f-49e3-b60d-5d48c7f02126",
"name": "메모4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3024,
1200
],
"parameters": {
"color": 6,
"width": 1040,
"height": 384,
"content": "# 🟢 Check Invoice on DB\n"
},
"typeVersion": 1
},
{
"id": "4996b730-0f80-4a37-a702-fc8588934ece",
"name": "메모5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1936,
672
],
"parameters": {
"color": 4,
"width": 1136,
"height": 1200,
"content": "# 🟢 Sends Data & Notify\n"
},
"typeVersion": 1
},
{
"id": "49782403-dd3c-4457-97ca-bf49c393c395",
"name": "메모7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1888,
2016
],
"parameters": {
"color": 5,
"width": 2384,
"height": 1072,
"content": "# 👤 Manual Review & Sends Data"
},
"typeVersion": 1
},
{
"id": "ff8438b9-5804-473b-bd6b-c9840be62317",
"name": "오류 트리거",
"type": "n8n-nodes-base.errorTrigger",
"position": [
-6048,
2192
],
"parameters": {},
"typeVersion": 1
},
{
"id": "fdaacf56-2efe-4272-80fc-365951cd7d48",
"name": "Create a database page",
"type": "n8n-nodes-base.notion",
"position": [
-5840,
2192
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "258f7557-9573-8013-bb51-d528ba507e68",
"cachedResultUrl": "https://www.notion.so/258f755795738013bb51d528ba507e68",
"cachedResultName": "ERROR CHECK"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Message|rich_text",
"textContent": "={{ $json.execution.error.message }}"
},
{
"key": "Workflow|title",
"title": "={{ $json.workflow.name }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
}
],
"pinData": {},
"connections": {
"Code": {
"main": [
[
{
"node": "Merge Data Value into One Key",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "5b13d0d1-28a9-47df-82a2-5b83d338c413",
"type": "main",
"index": 0
}
]
]
},
"Aggregate": {
"main": [
[
{
"node": "cb2e3474-2c42-4804-a1a0-aea65c45aece",
"type": "main",
"index": 0
}
]
]
},
"Aggregate1": {
"main": [
[
{
"node": "d707c4fc-d2a5-496d-9d6b-8c71f627ad21",
"type": "main",
"index": 0
}
]
]
},
"Aggregate2": {
"main": [
[
{
"node": "2d9bf727-1b2f-4a1e-bc54-61c686f63d01",
"type": "main",
"index": 0
}
]
]
},
"Aggregate3": {
"main": [
[
{
"node": "7766fd88-d419-47ee-b545-e6d6d574045f",
"type": "main",
"index": 0
}
]
]
},
"Aggregate4": {
"main": [
[
{
"node": "cb059bc5-42c4-4eee-a4c5-f70013b81d1f",
"type": "main",
"index": 0
}
]
]
},
"Aggregate5": {
"main": [
[
{
"node": "be11d35c-a96d-432d-bf60-747e9242b8b0",
"type": "main",
"index": 0
}
]
]
},
"Aggregate6": {
"main": [
[
{
"node": "75d16382-b71a-413c-abb7-5f6ee529b268",
"type": "main",
"index": 0
}
]
]
},
"Aggregate7": {
"main": [
[
{
"node": "38fd6b37-35cd-43f4-a6a1-caf4c20f0848",
"type": "main",
"index": 0
}
]
]
},
"Aggregate8": {
"main": [
[
{
"node": "852ca201-dfe6-4d5b-bf6d-9d06954f0604",
"type": "main",
"index": 0
}
]
]
},
"Aggregate9": {
"main": [
[
{
"node": "3fbead5e-2aa0-4222-a2ff-2a03901ffbe0",
"type": "main",
"index": 0
}
]
]
},
"Aggregate10": {
"main": [
[
{
"node": "2731b665-b116-459e-9a90-1d28d63d1c85",
"type": "main",
"index": 0
}
]
]
},
"Aggregate11": {
"main": [
[
{
"node": "b206f8e8-74f5-4502-8dc1-ef154867890f",
"type": "main",
"index": 0
}
]
]
},
"fa73399a-c133-48b3-87fe-922961f4d448": {
"main": [
[
{
"node": "1e7efdbb-f58c-4cfa-9a74-22cda80b58c7",
"type": "main",
"index": 0
}
],
[
{
"node": "5487835c-5655-476b-bbad-f368653150d1",
"type": "main",
"index": 0
}
],
[
{
"node": "8f25494a-6b7c-45e5-99fc-7ab5e2e0d085",
"type": "main",
"index": 0
}
],
[
{
"node": "48bc9ab1-03bb-40a6-844d-a4fff203fcd5",
"type": "main",
"index": 0
}
],
[
{
"node": "ed421b2f-4a7e-4098-9d09-96965813f3e4",
"type": "main",
"index": 0
}
],
[
{
"node": "8e8ac821-5cd5-4bd2-b78d-203fd5cf57fb",
"type": "main",
"index": 0
}
],
[
{
"node": "da9cdafe-01c8-49c8-afaa-29cb46f67aef",
"type": "main",
"index": 0
}
]
]
},
"ca8559e4-b29b-4a1a-b113-e70bba47f23a": {
"main": [
[
{
"node": "7dde97e8-b6cd-4527-a948-36d62ca9d235",
"type": "main",
"index": 0
}
],
[
{
"node": "6543e986-8860-457d-a8de-bb052718e9c6",
"type": "main",
"index": 0
}
]
]
},
"909b0322-55e4-4c3d-a5a9-9392d5bbf745": {
"main": [
[
{
"node": "ca8559e4-b29b-4a1a-b113-e70bba47f23a",
"type": "main",
"index": 0
}
]
]
},
"Error Trigger": {
"main": [
[
{
"node": "fdaacf56-2efe-4272-80fc-365951cd7d48",
"type": "main",
"index": 0
}
]
]
},
"Slack Trigger": {
"main": [
[
{
"node": "909b0322-55e4-4c3d-a5a9-9392d5bbf745",
"type": "main",
"index": 0
}
]
]
},
"5b13d0d1-28a9-47df-82a2-5b83d338c413": {
"main": [
[
{
"node": "d6375c43-4c99-41fe-9215-eec2ba228e17",
"type": "main",
"index": 0
}
],
[
{
"node": "5fcebb58-8279-4d89-981f-27d34f2c2747",
"type": "main",
"index": 0
}
],
[
{
"node": "6342d55e-4e82-4755-8826-3d4e5b7f9be7",
"type": "main",
"index": 0
}
]
]
},
"da9cdafe-01c8-49c8-afaa-29cb46f67aef": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"d165164c-bcf8-4ef3-b434-9416865c6b26": {
"main": [
[
{
"node": "Aggregate4",
"type": "main",
"index": 0
}
]
]
},
"Basic LLM Chain": {
"main": [
[
{
"node": "54187b60-dabd-42a7-b5c9-d46199dbed73",
"type": "main",
"index": 0
}
]
]
},
"1c81ef35-2d9b-435b-90ae-979dbebe8ace": {
"main": [
[
{
"node": "1ed7bced-88d4-4210-b323-c64bf1c1fece",
"type": "main",
"index": 0
}
]
]
},
"c6f37078-96cb-4e97-b678-ebb688afb413": {
"main": [
[
{
"node": "0163f077-3850-4609-9def-1483cd5f720b",
"type": "main",
"index": 0
}
],
[
{
"node": "237841d5-2268-4efe-9ae2-25e904dbd48a",
"type": "main",
"index": 0
}
]
]
},
"2b1a1c57-bad7-4629-a969-aced98962828": {
"main": [
[
{
"node": "a9a19c40-4875-4c02-9c2b-3f65124ebdec",
"type": "main",
"index": 0
}
],
[
{
"node": "0af156a4-3324-4a1f-9f63-7a181adb4485",
"type": "main",
"index": 0
}
]
]
},
"de2b4a0f-eaba-4d8c-ac76-220f14725f10": {
"main": [
[
{
"node": "Merge Items Invoice",
"type": "main",
"index": 0
}
]
]
},
"af88d22a-5a2d-4471-9ac9-b1cb9531bd35": {
"main": [
[
{
"node": "acb3e33c-59de-4cf3-a008-a47af9673abb",
"type": "main",
"index": 0
}
]
]
},
"2d7330e7-e3b9-4afc-9217-9dcbdbd4a3e9": {
"main": [
[
{
"node": "Aggregate8",
"type": "main",
"index": 0
}
]
]
},
"7dde97e8-b6cd-4527-a948-36d62ca9d235": {
"main": [
[
{
"node": "1c81ef35-2d9b-435b-90ae-979dbebe8ace",
"type": "main",
"index": 0
}
]
]
},
"54187b60-dabd-42a7-b5c9-d46199dbed73": {
"main": [
[
{
"node": "0e729384-88a1-4ee3-9bd1-ccc218462dbc",
"type": "main",
"index": 0
}
]
]
},
"c3b08864-40e6-4c71-b0ad-f46231351a92": {
"main": [
[
{
"node": "de2b4a0f-eaba-4d8c-ac76-220f14725f10",
"type": "main",
"index": 0
}
]
]
},
"Merge Items Invoice": {
"main": [
[
{
"node": "3b8e7c65-8d40-4636-bfb0-8091365e584e",
"type": "main",
"index": 0
}
]
]
},
"1ed7bced-88d4-4210-b323-c64bf1c1fece": {
"main": [
[
{
"node": "Stop and Error",
"type": "main",
"index": 0
}
],
[
{
"node": "Merge Data Value into One Key",
"type": "main",
"index": 0
}
]
]
},
"acb3e33c-59de-4cf3-a008-a47af9673abb": {
"main": [
[
{
"node": "Stop and Error1",
"type": "main",
"index": 0
}
],
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"88aae252-885b-4433-af13-88fea42ad24e": {
"main": [
[
{
"node": "c3b08864-40e6-4c71-b0ad-f46231351a92",
"type": "main",
"index": 0
}
],
[
{
"node": "e4d849f2-4c10-4e93-9133-b78e861687f2",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Chat Model4": {
"ai_languageModel": [
[
{
"node": "Basic LLM Chain",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"d6375c43-4c99-41fe-9215-eec2ba228e17": {
"main": [
[
{
"node": "c6f37078-96cb-4e97-b678-ebb688afb413",
"type": "main",
"index": 0
}
]
]
},
"5fcebb58-8279-4d89-981f-27d34f2c2747": {
"main": [
[
{
"node": "2b1a1c57-bad7-4629-a969-aced98962828",
"type": "main",
"index": 0
}
]
]
},
"6342d55e-4e82-4755-8826-3d4e5b7f9be7": {
"main": [
[
{
"node": "2e9ab2da-f324-4864-8617-4ebf6d6873ab",
"type": "main",
"index": 0
}
]
]
},
"a1eaf719-3843-424f-b758-b58b32cdc30c": {
"main": [
[
{
"node": "Aggregate7",
"type": "main",
"index": 0
}
]
]
},
"Merge with Original Data": {
"main": [
[
{
"node": "88aae252-885b-4433-af13-88fea42ad24e",
"type": "main",
"index": 0
}
]
]
},
"8f25494a-6b7c-45e5-99fc-7ab5e2e0d085": {
"main": [
[
{
"node": "35db92f5-faf6-4ecd-a148-7bbf09bf620b",
"type": "main",
"index": 0
}
]
]
},
"cca27d76-da70-42fc-b6b2-5c5330490d2b": {
"main": [
[
{
"node": "Aggregate1",
"type": "main",
"index": 0
}
]
]
},
"5e962f4f-f35a-46f6-a80f-bdd66553c3ed": {
"main": [
[
{
"node": "Aggregate10",
"type": "main",
"index": 0
}
]
]
},
"f90f09ea-6016-429b-915f-1565bcff1d97": {
"main": [
[
{
"node": "Aggregate",
"type": "main",
"index": 0
}
]
]
},
"0163f077-3850-4609-9def-1483cd5f720b": {
"main": [
[
{
"node": "557a172b-f58a-4f82-a8b8-ddc3865bbe20",
"type": "main",
"index": 0
}
]
]
},
"8e8ac821-5cd5-4bd2-b78d-203fd5cf57fb": {
"main": [
[
{
"node": "25951d6e-c27d-48b0-8bef-1b06bb931c30",
"type": "main",
"index": 0
}
]
]
},
"54b5f427-4c28-4b83-a04d-40bc14df309d": {
"main": [
[
{
"node": "Aggregate2",
"type": "main",
"index": 0
}
]
]
},
"0af156a4-3324-4a1f-9f63-7a181adb4485": {
"main": [
[
{
"node": "a1eaf719-3843-424f-b758-b58b32cdc30c",
"type": "main",
"index": 0
}
]
]
},
"35db92f5-faf6-4ecd-a148-7bbf09bf620b": {
"main": [
[
{
"node": "Aggregate3",
"type": "main",
"index": 0
}
]
]
},
"Merge Data Value into One Key": {
"main": [
[
{
"node": "Basic LLM Chain",
"type": "main",
"index": 0
}
]
]
},
"1e7efdbb-f58c-4cfa-9a74-22cda80b58c7": {
"main": [
[
{
"node": "f90f09ea-6016-429b-915f-1565bcff1d97",
"type": "main",
"index": 0
}
]
]
},
"5487835c-5655-476b-bbad-f368653150d1": {
"main": [
[
{
"node": "a09de3d7-f79a-4027-ab49-a4e2376d3f82",
"type": "main",
"index": 0
}
]
]
},
"237841d5-2268-4efe-9ae2-25e904dbd48a": {
"main": [
[
{
"node": "010957b9-c7a4-4432-ac02-6ea1400f0418",
"type": "main",
"index": 0
}
]
]
},
"ed421b2f-4a7e-4098-9d09-96965813f3e4": {
"main": [
[
{
"node": "3f33a49f-f4d5-4483-b01b-ab7b05f1edef",
"type": "main",
"index": 0
}
]
]
},
"a9a19c40-4875-4c02-9c2b-3f65124ebdec": {
"main": [
[
{
"node": "Aggregate9",
"type": "main",
"index": 0
}
]
]
},
"3b8e7c65-8d40-4636-bfb0-8091365e584e": {
"main": [
[
{
"node": "fa73399a-c133-48b3-87fe-922961f4d448",
"type": "main",
"index": 0
}
]
]
},
"6543e986-8860-457d-a8de-bb052718e9c6": {
"main": [
[
{
"node": "af88d22a-5a2d-4471-9ac9-b1cb9531bd35",
"type": "main",
"index": 0
}
]
]
},
"557a172b-f58a-4f82-a8b8-ddc3865bbe20": {
"main": [
[
{
"node": "Aggregate11",
"type": "main",
"index": 0
}
]
]
},
"e4d849f2-4c10-4e93-9133-b78e861687f2": {
"main": [
[
{
"node": "e2179126-0881-4701-9d4a-62d335a74abd",
"type": "main",
"index": 0
}
]
]
},
"a09de3d7-f79a-4027-ab49-a4e2376d3f82": {
"main": [
[
{
"node": "54b5f427-4c28-4b83-a04d-40bc14df309d",
"type": "main",
"index": 0
}
]
]
},
"010957b9-c7a4-4432-ac02-6ea1400f0418": {
"main": [
[
{
"node": "5e962f4f-f35a-46f6-a80f-bdd66553c3ed",
"type": "main",
"index": 0
}
]
]
},
"0e729384-88a1-4ee3-9bd1-ccc218462dbc": {
"main": [
[
{
"node": "Merge with Original Data",
"type": "main",
"index": 0
}
]
]
},
"a988e2c7-ad51-4df3-b4d6-9d5cd6e123af": {
"main": [
[
{
"node": "Aggregate5",
"type": "main",
"index": 0
}
]
]
},
"48bc9ab1-03bb-40a6-844d-a4fff203fcd5": {
"main": [
[
{
"node": "d165164c-bcf8-4ef3-b434-9416865c6b26",
"type": "main",
"index": 0
}
]
]
},
"2e9ab2da-f324-4864-8617-4ebf6d6873ab": {
"main": [
[
{
"node": "2d7330e7-e3b9-4afc-9217-9dcbdbd4a3e9",
"type": "main",
"index": 0
}
]
]
},
"25951d6e-c27d-48b0-8bef-1b06bb931c30": {
"main": [
[
{
"node": "Aggregate6",
"type": "main",
"index": 0
}
]
]
},
"3f33a49f-f4d5-4483-b01b-ab7b05f1edef": {
"main": [
[
{
"node": "a988e2c7-ad51-4df3-b4d6-9d5cd6e123af",
"type": "main",
"index": 0
}
]
]
},
"e2179126-0881-4701-9d4a-62d335a74abd": {
"main": [
[
{
"node": "cca27d76-da70-42fc-b6b2-5c5330490d2b",
"type": "main",
"index": 0
}
]
]
}
}
}자주 묻는 질문
이 워크플로우를 어떻게 사용하나요?
위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.
이 워크플로우는 어떤 시나리오에 적합한가요?
고급 - AI 요약, 멀티모달 AI
유료인가요?
이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.
관련 워크플로우 추천
매일 WhatsApp 그룹 지능형 분석: GPT-4.1 분석 및 음성 메시지 변환
매일 WhatsApp 그룹 지능 분석: GPT-4.1 분석 및 음성 메시지 트랜스크립션
If
Set
Code
+
If
Set
Code
52 노드Daniel Lianes
기타
AI 기반 회의 연구 및 일일 아젠다 (Google 캘린더, Attio CRM 및 Slack)
AI 기반 회의 연구 및 일일 아젠다: Google 캘린더, Attio CRM 및 Slack 활용
If
Set
Code
+
If
Set
Code
30 노드Harry Siggins
AI 요약
Gitlab 코드 리뷰 템플릿
Gemini AI와 JIRA 컨텍스트를 사용한 GitLab 병합 요청 코드 리뷰 자동화
If
Set
Code
+
If
Set
Code
41 노드Evgeny Agronsky
AI 요약
PDF에서 주문으로
AI를 사용한 PDF 구매 주문서를 Adobe Commerce 판매 주문서로 자동 변환
If
Set
Code
+
If
Set
Code
96 노드JKingma
문서 추출
WordPress 블로그 자동화 프로페셔널 에디션(심층 연구) v2.1 마켓
GPT-4o, Perplexity AI 및 다국어 지원을 사용한 SEO 최적화 블로그 생성 자동화
If
Set
Xml
+
If
Set
Xml
125 노드Daniel Ng
콘텐츠 제작
제품 이미지에서 UGC 비디오 생성 (Gemini와 VEO3)
Gemini와 VEO3를 사용하여 제품 이미지에서 UGC 비디오 생성
Set
Code
Wait
+
Set
Code
Wait
32 노드Growth AI
콘텐츠 제작