- Added support for deduplication of orders based on the `email_message_id`, preventing duplicate entries during email intake. - Introduced the `displayContactName` utility to sanitize and format contact names, improving the display of order details by removing extraneous whitespace. - Updated relevant order-related components to utilize the new utility for consistent contact name rendering. - Enhanced tests to cover the new deduplication logic and the functionality of the `displayContactName` utility, ensuring robust behavior in various scenarios.
10244 lines
No EOL
361 KiB
JSON
10244 lines
No EOL
361 KiB
JSON
{
|
|
"updatedAt": "2026-08-18T21:57:21.328Z",
|
|
"createdAt": "2026-08-15T22:15:05.317Z",
|
|
"id": "DWkxko0SS1uoelsg",
|
|
"name": "WFDASH \u2014 ORDER MONITOR (Unified: Single + Multi-Order)",
|
|
"description": "WFDASH \u2014 Populate Item Intel (Live)",
|
|
"active": true,
|
|
"isArchived": false,
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Normalize Image Text\"\nconst item = $input.first();\nconst extractedText = item.json.content?.[0]?.text || '';\nreturn [{\n json: {\n ...item.json,\n text: extractedText\n }\n}];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1136,
|
|
3040
|
|
],
|
|
"id": "4bdaa1df-2d49-4efb-a911-79c180707823",
|
|
"name": "Code in JavaScript3"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "image",
|
|
"modelId": {
|
|
"__rl": true,
|
|
"value": "MiniMax-M3",
|
|
"mode": "list",
|
|
"cachedResultName": "MiniMax-M3"
|
|
},
|
|
"inputType": "binary",
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"options": {}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.anthropic",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
912,
|
|
3040
|
|
],
|
|
"id": "65a1b6ea-7eda-475a-8750-0862bafcb308",
|
|
"name": "Analyze image1",
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "jLsa9I3U2W6vqzeE",
|
|
"name": "Anthropic account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
2144
|
|
],
|
|
"id": "c7ae6cf1-0d69-4405-a9f0-99d84c6a7d3f",
|
|
"name": "Extract from CSV1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "text",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
2848
|
|
],
|
|
"id": "4766ba0f-b5e5-434d-af89-c0acf6e0eb56",
|
|
"name": "Extract from TXT1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xlsx",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
2656
|
|
],
|
|
"id": "4a6f899a-3636-4068-8c55-da6d74c01ec3",
|
|
"name": "Extract from XLSX1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xls",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
2336
|
|
],
|
|
"id": "8df30e7b-72f8-4f97-89ea-50b5a117f8e3",
|
|
"name": "Extract from XLS1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "application/pdf",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
},
|
|
"id": "72a24273-44ef-4691-9a59-feabeb2663f3"
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "34b35229-1280-49bd-8a0a-d17025841a70",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/csv",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "71fff6af-219b-4b35-85e9-3f3d6ce852f6",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/xls",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "8e69249c-b0ef-4a08-9af5-a7afc25c02e4",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/xlsx",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "7c11e0a4-6884-4c5b-ad46-ebc76b9b68cb",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "=text/text",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "41403feb-e786-488c-b1d4-eecd007e3cd0",
|
|
"leftValue": "={{ $('Limit').item.json.text || $('Limit').item.json.textContent }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "751252c4-116c-42f3-9709-ddbd4a372423",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "image/png",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "11532f4b-1b10-4596-b255-b824aec56c1f",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "image/jpeg",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "124a0978-e621-43a2-b244-54be8da0e30b",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "application/octet-stream",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {
|
|
"allMatchingOutputs": true
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
240,
|
|
2304
|
|
],
|
|
"id": "57c97b41-8fa5-4f77-a849-d4320a6c4702",
|
|
"name": "Switch1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Split Order Items\"\nconst orderId = $('Create orders').item.json.id;\nconst items = $('AI Agent').item.json.output.order_items;\n\nreturn items.map(item => ({\n json: {\n order: orderId,\n description: item.description,\n quantity: item.quantity,\n selling_price: item.unit_price,\n notes: item.notes\n }\n}));\n"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3248,
|
|
2032
|
|
],
|
|
"id": "0a6ad492-45d7-4849-8828-f16abb29ff4d",
|
|
"name": "Code in JavaScript2"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "order_items",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "order-1",
|
|
"name": "order",
|
|
"value": "={{ $json.order }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "description-1",
|
|
"name": "description",
|
|
"value": "={{ $json.description }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "quantity-1",
|
|
"name": "quantity",
|
|
"value": "={{ $json.quantity }}",
|
|
"type": "number"
|
|
},
|
|
{
|
|
"id": "notes-1",
|
|
"name": "notes",
|
|
"value": "={{ $json.notes }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "d29a84cf-bb1e-4cfd-8665-3c4164aa3103",
|
|
"name": "item_status",
|
|
"value": "pending",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "0b83a5b4-d1f5-43fc-a712-909ddabe3bb0",
|
|
"name": "selling_price",
|
|
"value": "={{ $json.selling_price }}",
|
|
"type": "number"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3472,
|
|
2032
|
|
],
|
|
"id": "3652cf1b-e203-4076-b5c5-6b6b2b48bbaf",
|
|
"name": "Create order_items1",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"mode": "combine",
|
|
"combineBy": "combineAll",
|
|
"options": {
|
|
"clashHandling": {
|
|
"values": {
|
|
"resolveClash": "addSuffix"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.merge",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
3696,
|
|
1424
|
|
],
|
|
"id": "a75be0b6-50c4-4b0f-af24-f466d871ee25",
|
|
"name": "Merge1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"collection": "=pbc_3527180448",
|
|
"action": "update",
|
|
"recordId": "={{ $json.orderId_1 }}",
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "=order",
|
|
"value": "={{ $json.id_2 }}"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"type": "n8n-nodes-pocketbase-admin.pocketBaseAdmin",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3920,
|
|
1616
|
|
],
|
|
"id": "75113d56-619f-4f5b-b912-8ba85549ab71",
|
|
"name": "PocketBase1",
|
|
"credentials": {
|
|
"pocketBaseAdminApi": {
|
|
"id": "UjiyPhf4vcCV6eE2",
|
|
"name": "PocketBase Admin account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "customers",
|
|
"parameters": {
|
|
"elementsPerPage": 12,
|
|
"filter": "={{ \n (() => {\n const name = $('AI Agent').item.json.output.order_info.contact_name || '';\n const email = $('AI Agent').item.json.output.order_info.order_info?.sender_email || $('AI Agent').item.json.output.order_info.sender_email || '';\n \n // 1. Clean and split the name keywords\n const nameKeywords = name.replace(/mr\\.|mrs\\.|ms\\.|mr|mrs|ms/gi, '').split(/[\\s,]+/).filter(w => w.length > 2);\n const nameQuery = nameKeywords.length > 0 ? nameKeywords.map(w => `customer ~ '${w}'`).join(' && ') : '';\n \n // 2. Build the email query if it exists\n const emailQuery = email ? `email ~ '${email}'` : '';\n \n // 3. Force a hard fallback ID only if both name and email are missing\n if (!nameQuery && !emailQuery) {\n return \"id = 'xmd68ps32k1x5fu'\";\n }\n \n // 4. Join name query and email query with || (OR)\n return [nameQuery, emailQuery].filter(Boolean).map(q => `(${q})`).join(' || ');\n })()\n}}"
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "b2da5fe5-9874-4510-86df-bf9fbd33f31c",
|
|
"name": "Search customers",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2800,
|
|
1952
|
|
],
|
|
"alwaysOutputData": true,
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "MiniMax-M2.5",
|
|
"options": {
|
|
"hideThinking": true,
|
|
"temperature": 0.1,
|
|
"timeout": 60000,
|
|
"maxRetries": 3
|
|
}
|
|
},
|
|
"id": "a4bf0ff2-1686-489e-bc15-2f5e205128f0",
|
|
"name": "Minimax Chat Model4",
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2304,
|
|
3072
|
|
],
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsonSchemaExample": "{\n \"order_info\": {\n \"company_name\": \"Harmony\",\n \"contact_name\": \"Mark Wilkie\",\n \"sender_email\": \"davidels1986@gmail.com\",\n \"rfq_number\": \"PO-20260604-004\",\n \"notes\": \"Please quote on Isherwood letterhead\"\n },\n \"order_items\": [\n {\n \"description\": \"TURBO END+ SUPER TOUGH COUNTERSINK 14-60MM\",\n \"quantity\": 2,\n \"unit_price\": 0.00,\n \"notes\": \"SKU: JEIETCSK-60\"\n }\n ]\n}",
|
|
"autoFix": true
|
|
},
|
|
"id": "e1f6cc88-900f-45e1-83f8-0bd28611d6ac",
|
|
"name": "Structured Output Parser",
|
|
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
2224,
|
|
2864
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"promptType": "define",
|
|
"text": "Email Subject: {{ $('Limit').item.json.envelope.subject }}\\n\\nSender Email: {{ $('Limit').item.json.envelope.sender[0].address }}\\n\\nRecipient Email: {{ $('Limit').item.json.envelope.to[0].address }}\\n\\n--- DETERMINISTIC CONTACT NAME (pre-extracted) ---\\n{{ $('Extract contact name (deterministic)').item.json.contact_name || '(none \u2014 extract from the documents below)' }}\\n\\n--- EMAIL BODY ---\\n{{ $('Limit').item.json.textContent }}\\n\\n\\n--- ORDER DETAILS ---\\n\\n\\n\\n--- ATTACHMENT TEXT (ALL PDFs COMBINED) ---\\n{{ $('Code in JavaScript4').item.json.aggregatedText }}\\n\\n\\n",
|
|
"hasOutputParser": true,
|
|
"options": {
|
|
"systemMessage": "You are an expert procurement and data extraction AI. Your task is to process incoming orders by analyzing both the email body and any attached document text.\n\nYou must reconcile the information. For example, if the email says \"Please quote the attached 5 items,\" you must extract the company details from the email signature and the 5 items from the attachment text.\n\nExtract the data and return it STRICTLY as a valid JSON object matching the exact structure below. Do not include markdown formatting (like ```json), conversational text, or explanations. Only return the JSON.\n\nJSON STRUCTURE:\n{\n \"order_info\": {\n \"company_name\": \"Extract the buyer/end-customer company name.\",\n \"contact_name\": \"Extract the actual End-User or Requester of the goods (Look for fields like 'Requester', 'Deliver To', 'Attention', or names in the Notes). Do NOT use the 'Buyer' or 'Created By' name if a Requester is listed.\",\n \"sender_email\": \"Extract the email address of the person requesting the quote.\",\n \"rfq_number\": \"Extract any visible RFQ, Reference, or Order number provided by the customer. Leave empty if none.\",\n \"notes\": \"Summarize any specific instructions, delivery requirements, or context from the email body (e.g., 'Urgent delivery required').\"\n },\n \"order_items\": [\n {\n \"description\": \"The exact name, SKU, or description of the requested item.\",\n \"quantity\": 1,\n \"unit_price\": 0.00,\n \"notes\": \"Any specific unit of measure (e.g., 'meters', 'kg') or specific notes for this single item.\"\n }\n ]\n}\n\nRULES:\n1. Do not invent items, prices, or quantities.\n2. If there are no line items found in either the email or the attachment, return an empty array for \"order_items\".\n3. Ensure numerical quantities and unit_prices are returned as clean numbers, not strings. Do not include currency symbols (e.g., return 660, not \"R660\" or \"ZAR 660\"). If a unit price is not provided, return 0.\n4. CUSTOMER EXTRACTION PRIORITY: You must look for the \"company_name\" and \"contact_name\" inside the ATTACHMENT TEXT first (e.g., the letterhead or \"Bill To\" section of the Purchase Order). Use the EMAIL BODY (e.g., email signature or instructions) as a fallback ONLY if these details are completely missing from the attachments.\n5. REQUESTER VS BUYER: Large corporate orders often list a Procurement Buyer (e.g., 'Current Buyer: Chagi') and a specific Requester (e.g., 'Requester/Deliver To: Dreyer, Mr. Petrus' or 'Notes: Req ..._P Dreyer'). You MUST extract the specific Requester as the contact_name. Only default to the Buyer if no Requester is mentioned anywhere in the document.\n6. The \"ATTACHMENT TEXT\" section may contain content from MULTIPLE separate attachments, each clearly marked with a header like \"=== ATTACHMENT X OF Y ===\". Each marked block is a DISTINCT document that may contain its own items, totals, and terms. You MUST extract items from EVERY block listed. Do not stop scanning after one document's \"Total\" or \"Terms\" section \u2014 continue past each \"=== ATTACHMENT ===\" marker and extract that document's items too. The final \"order_items\" array must include items from ALL attachments combined.\\n\\n7. CONTACT_NAME PRIORITY: If the 'DETERMINISTIC CONTACT NAME' section above contains a non-empty name (anything that is NOT '(none \u2014 extract from the documents below)'), use that value verbatim as `order_info.contact_name`. The deterministic extractor is more reliable than the LLM for the contact name (see the audit report). Only fall back to extracting from the email signature or PDF Requester/Deliver To/Attention lines if the deterministic name is missing.\\n8. EMPTY WHEN UNCERTAIN: If you cannot find a contact name ANYWHERE (neither in the deterministic section, nor the email signature, nor the PO requester line), return `contact_name: \"\"` \u2014 do NOT return placeholder strings like 'Unknown', 'Not specified', 'N/A', or the company name. Placeholder values are rejected by the operator UI and create duplicate customer matches.\\n9. STRIP WHITESPACE: If you do extract a name, return it with no trailing newline, no leading whitespace, and no trailing whitespace. The DB stores the value verbatim; stray whitespace renders as a dangling separator dot on the orders list.\\n",
|
|
"passthroughBinaryImages": false
|
|
}
|
|
},
|
|
"id": "1258e927-e791-43e1-82ba-f276ee503b0d",
|
|
"name": "AI Agent",
|
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
"typeVersion": 3.1,
|
|
"position": [
|
|
2112,
|
|
2624
|
|
],
|
|
"retryOnFail": true,
|
|
"waitBetweenTries": 5000
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {
|
|
"hideThinking": true,
|
|
"temperature": 0.1,
|
|
"timeout": 100000,
|
|
"maxRetries": 3
|
|
}
|
|
},
|
|
"id": "5354d113-eebb-4a68-b648-82dd6439fe89",
|
|
"name": "Minimax Chat Model",
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2096,
|
|
2864
|
|
],
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "pdf",
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"options": {
|
|
"joinPages": true,
|
|
"keepSource": "both"
|
|
}
|
|
},
|
|
"id": "674be490-fc4e-479b-b885-4cc280ed3dbf",
|
|
"name": "Extract PDF",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
464,
|
|
1936
|
|
],
|
|
"onError": "continueRegularOutput"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Turn one email item with N binary attachments into N items,\n// each carrying exactly one binary, plus the original email json.\nconst item = $input.first();\nconst binaryKeys = Object.keys(item.binary || {});\n\nif (binaryKeys.length === 0) {\n // no attachments \u2014 pass the email through with an empty marker\n return [{ json: { ...item.json, hasAttachments: false, totalAttachments: 0 }, binary: {} }];\n}\n\nreturn binaryKeys.map((key, index) => ({\n json: {\n ...item.json,\n binaryKey: key,\n fileName: item.binary[key].fileName,\n mimeType: item.binary[key].mimeType,\n fileSize: item.binary[key].fileSize,\n attachmentIndex: index,\n totalAttachments: binaryKeys.length,\n hasAttachments: true\n },\n binary: { [key]: item.binary[key] }\n}));"
|
|
},
|
|
"id": "51bca253-9383-400b-8118-9941cc1373d7",
|
|
"name": "Split Attachments1",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
16,
|
|
2416
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"numberInputs": 7
|
|
},
|
|
"type": "n8n-nodes-base.merge",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1360,
|
|
2544
|
|
],
|
|
"id": "8e4b7968-edb8-4ad2-9370-bbf860d64cea",
|
|
"name": "Merge2"
|
|
},
|
|
{
|
|
"parameters": {},
|
|
"type": "n8n-nodes-base.limit",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-432,
|
|
2416
|
|
],
|
|
"id": "5920ed39-2773-4127-a6ac-2d5747013b7c",
|
|
"name": "Limit"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "fa209857-8e0c-400d-8d18-5cad78d5a988",
|
|
"leftValue": "={{ $json.output.order_items.length }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEquals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
2576,
|
|
2624
|
|
],
|
|
"id": "79a5e452-ff6a-4338-a2a1-b07449024198",
|
|
"name": "If1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "moveEmail",
|
|
"sourceMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $('Limit').item.json.attributes?.uid || $('Limit').item.json.uid }}",
|
|
"destinationMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Failed",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Failed"
|
|
}
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3568,
|
|
2800
|
|
],
|
|
"id": "ce07cbfd-5159-4333-98a9-f65d13969eba",
|
|
"name": "MoveEmail email1",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"workflowId": {
|
|
"__rl": true,
|
|
"value": "Fm1c24RCfEQt9Qkn",
|
|
"mode": "list",
|
|
"cachedResultUrl": "/workflow/Fm1c24RCfEQt9Qkn",
|
|
"cachedResultName": "WFDASH \u2014 Master Drilling Price Matcher (sub-workflow)"
|
|
},
|
|
"workflowInputs": {
|
|
"mappingMode": "defineBelow",
|
|
"value": {},
|
|
"matchingColumns": [],
|
|
"schema": [],
|
|
"attemptToConvertTypes": false,
|
|
"convertFieldsToString": true
|
|
},
|
|
"options": {
|
|
"waitForSubWorkflow": false
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.executeWorkflow",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
6560,
|
|
2080
|
|
],
|
|
"id": "083d87f5-4684-4394-8386-a8dd0fb020cc",
|
|
"name": "Execute Workflow"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "fca9dae9-453b-4481-8fcb-86a53b051aab",
|
|
"leftValue": "={{ $('Limit').first().json.envelope.sender[0].address }}",
|
|
"rightValue": "masterdrilling",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
6336,
|
|
2032
|
|
],
|
|
"id": "b5da54e3-3288-42a9-9bcc-b05dfdcd2026",
|
|
"name": "If2"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"mailboxes": [
|
|
"INBOX"
|
|
],
|
|
"emailDateRange": {},
|
|
"emailFlags": {
|
|
"flagged": false,
|
|
"seen": false
|
|
},
|
|
"customLabels": {},
|
|
"emailSearchFilters": {},
|
|
"includeParts": [
|
|
"textContent",
|
|
"htmlContent",
|
|
"markdownContent",
|
|
"attachmentsInfo",
|
|
"flags",
|
|
"size",
|
|
"bodyStructure",
|
|
"headers"
|
|
]
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1904,
|
|
1456
|
|
],
|
|
"id": "4bf25239-7d91-4733-ad14-a4d84f992f42",
|
|
"name": "CHECK FOR NEW MAIL",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build the user message for the AI Agent.\n//\n// Inputs:\n// - Aggregate output: { data: [item, item, item] } \u2190 items\n// - Upstream \"Create orders\" output: full parent order\n//\n// Output: { userMessage, target, parentId, items }\n// - userMessage: the assembled prompt string (\u2192 AI Agent)\n// - target: \"order\"\n// - parentId: parent order id (\u2192 PATCH-back to set ai_insights)\n// - items: line item ids (\u2192 PATCH-back loop to set ai_item_insights)\n\nconst data = $json.data;\nif (!data || !Array.isArray(data) || data.length === 0) {\n throw new Error('Aggregate returned no data \u2014 check the upstream Aggregate node');\n}\n\n// Pull the parent from the Create orders node. Rename the node in the\n// expression below if yours is named differently (e.g. \"PocketBase1\").\nconst parent = $('Create orders').first().json;\nif (!parent || !parent.id) {\n throw new Error(\n 'Parent order not found \u2014 check the node name in $(\"Create orders\")',\n );\n}\n\nconst supplierIds = new Set();\nfor (const it of items) {\n if (it.supplier) supplierIds.add(it.supplier);\n}\n\nconst ref = parent.order_number || '(not set)';\n// expand is included only if the parent was fetched with ?expand=\n// \u2014 falls back to the id if not.\nconst companyName = parent.expand?.company?.company || '(not set)';\nconst customerName = parent.expand?.customer?.customer || '(not set)';\n\nlet msg = `Analyse this order and produce insights for the parent and for each line item.\\n\\n`;\nmsg += `## Parent record\\n\\n`;\nmsg += `id: ${parent.id}\\n`;\nmsg += `order_number: ${ref}\\n`;\nmsg += `company_id: ${parent.company || '(not set)'}\\n`;\nmsg += `company_name: ${companyName}\\n`;\nmsg += `customer_id: ${parent.customer || '(not set)'}\\n`;\nmsg += `customer_name: ${customerName}\\n`;\nmsg += `status: ${parent.status || '(not set)'}\\n`;\nmsg += `created: ${parent.created || '(not set)'}\\n`;\nmsg += `notes: ${parent.notes || '(none)'}\\n\\n`;\n\n// \u2500\u2500 Fetch sprint categories from PocketBase \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet categoriesText = '(no active categories)';\ntry {\n const catRes = await $http.request({\n method: 'GET',\n url: '/api/collections/categories/records',\n params: {\n filter: 'is_active = true',\n sort: 'display_order',\n fields: 'id,name,slug',\n perPage: 100,\n },\n });\n if (catRes.response.statusCode === 200) {\n const cats = catRes.data?.items || [];\n if (cats.length > 0) {\n const lines = cats.map(c => ` - ${c.name} (slug: ${c.slug || c.id})`);\n categoriesText = lines.join('\\n');\n }\n }\n} catch (err) {\n // Non-fatal \u2014 the workflow still produces a useful userMessage\n categoriesText = '(category fetch failed \u2014 assign manually in the sprint)';\n}\n\nmsg += `## Line items (${data.length})\\n\\n`;\nfor (const it of data) {\n msg += `- id: ${it.id}\\n`;\n msg += ` description: ${it.description || '(empty)'}\\n`;\n msg += ` sku: ${it.sku || '(not set)'}\\n`;\n msg += ` quantity: ${it.quantity ?? '(not set)'}\\n`;\n msg += ` cost_price: ${it.cost_price ?? '(not set)'}\\n`;\n msg += ` selling_price: ${it.selling_price ?? '(not set)'}\\n`;\n msg += ` supplier_id: ${it.supplier || '(not set)'}\\n\\n`;\n}\n\nreturn [{\n json: {\n userMessage: msg,\n target: 'order',\n parentId: parent.id,\n items: data.map(it => ({ id: it.id })),\n },\n}];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3920,
|
|
2032
|
|
],
|
|
"id": "6c4fb948-b1d9-40ca-a45d-f753bd11b9d9",
|
|
"name": "MESSAGE FOR AI",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"aggregate": "aggregateAllItemData",
|
|
"include": "specifiedFields",
|
|
"fieldsToInclude": "category, collectionId, collectionName, cost_price, created, description, id, markup, matched_price_list_id, quantity, quote, quote_description, quote_number, selling_price, sku, supplier",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.aggregate",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3696,
|
|
2032
|
|
],
|
|
"id": "df65d61d-1f86-468c-b62d-729c7d129f9b",
|
|
"name": "Aggregate",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Split the validated AI output into parent + per-item patches.\n// The Structured Output Parser guarantees the shape, so no\n// defensive parsing is needed here.\n\nconst data = $json.output ?? $json; // parser wraps in .output\nconst upstream = $(\"MESSAGE FOR AI\").first().json;\nconst parentId = upstream.parentId;\nconst expectedItemIds = new Set((upstream.items || []).map(it => it.id));\n\nif (!parentId) {\n throw new Error(\"MESSAGE FOR AI didn't carry parentId\");\n}\n\nconst parentData = data.parent || {};\nconst parentPatch = {\n ...parentData,\n summary: data.summary || parentData.summary || \"\",\n};\n\nconst itemsOut = [];\nfor (const aiItem of data.items || []) {\n if (!aiItem?.id || !expectedItemIds.has(aiItem.id)) continue;\n const { id, ...rest } = aiItem;\n // NOTE: each returned item MUST be { json: {...} } \u2014 n8n validates\n // top-level keys and rejects bare objects. The error you saw\n // (\"Unknown top-level item key: kind\") was because of this.\n itemsOut.push({\n json: {\n kind: \"item\",\n itemId: id,\n aiItemInsights: rest,\n },\n });\n}\n\nreturn [\n { json: { kind: \"parent\", parentId, aiInsights: parentPatch } },\n ...itemsOut,\n];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5664,
|
|
2032
|
|
],
|
|
"id": "46768290-760f-4c73-852d-a48cd7043d93",
|
|
"name": "Parse Ai Resposes",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"promptType": "define",
|
|
"text": "={{ $json.userMessage }}",
|
|
"hasOutputParser": true,
|
|
"needsFallback": true,
|
|
"options": {
|
|
"systemMessage": "You are a quoting and ordering analyst for a parts distributor.\nYour job is to look at one order and produce a\nstructured review that helps the operator decide what to do next.\n\n# Tools you have\n\nYou can query the company's order book and inventory in PocketBase.\nYou have ONE tool per collection. Each tool supports at least `filter`,\n`sort`, and `limit`. Use them.\n\n- pb_quotes \u2014 historical quotes\n- pb_quote_items \u2014 historical quote line items (filter by quote id, sku, or description)\n- pb_orders \u2014 historical orders\n- pb_order_items \u2014 historical order line items (filter by order id, sku, or description)\n- pb_stock \u2014 current stock for a SKU or by description\n- pb_price_list \u2014 cost & sell history (filter by search_text, description, or sku)\n\nEach tool returns PB records. Record ids are 15-character strings like\n`pbc_xxxxx` or `q1w2e3r4t5y6u7i` \u2014 these are the only valid `ref_id`\nvalues. Never invent ids.\n\n# Matching items without a SKU \u2014 DEFAULT BEHAVIOUR\n\nMost new quotes arrive WITHOUT a SKU. The line item only has a\ndescription like \"O19 PPE NOTICE BOARD SIGN 300X300 PVC\". You MUST\nmatch these by description. \"No match\" is not an acceptable answer\nfor an item that has any description text.\n\n## Step 1 \u2014 Build keywords\n\nTake the description, drop noise:\n \"O19 - PPE NOTICE BOARD SIGN 300 X 300 PVC\" \u2192\n \"ppe notice board sign pvc 300\"\n\n## Step 2 \u2014 Search price_list (ALWAYS do this first)\n\n filter: search_text ~ \"ppe notice board\" (try 2-3 word combos)\n\n`search_text` is a pre-computed, lowercased, no-spaces field that\nexists specifically for this. Every price_list record has it.\n\nIf the first keyword combo returns 0 records, try:\n - A different 2-word combo (e.g. \"notice sign\", \"ppe pvc\")\n - A 1-word combo (e.g. \"ppe\", \"pvc\")\n - `description ~ \"ppe\"` (raw description, case-insensitive contains)\n\n## Step 3 \u2014 Verify the candidate\n\nIf you find 1-5 candidates, pick the one whose `description`\nmatches the line item best. If you find 5+, narrow the keywords.\n\n## Step 4 \u2014 Include the match in the output\n\nThe item summary MUST mention the matched price_list record:\n\n \"Matches price list 'O19 - PPE NOTICE BOARD SIGN 300 X 300 PVC'\n (cost R110, sell R165, stock 25, supplier DESIGN STUDIO).\n Source: id 11gkmah2bxgwi6f.\"\n\n## Step 5 \u2014 Only if you have a SKU on the line item\n\nIf (and only if) the line item has a non-empty `sku` field,\nalso try:\n filter: sku = \"<sku>\" (exact, 1 record usually)\n filter: sku_normalised = \"<sku-no-dashes>\" (fallback)\n\nBut description search FIRST, always.\n\n# pb_quote_items + pb_order_items \u2014 matching without SKU\n\nWhen the line item has no SKU, also search historical records by\ndescription text:\n\n filter: description ~ \"<2-3 most specific words from description>\"\n\nCombine the words with `&&`. Example for \"PPE NOTICE BOARD SIGN\":\n\n filter: description ~ \"PPE\" && description ~ \"NOTICE\"\n\nThis catches items that were previously quoted with similar\ndescriptions (e.g. \"115 FLAPPER DISC P60\") even when no SKU\nexists in either the historical quote_item or the price list.\n\n# Input you will receive\n\nThe user message contains:\n- target: \"order\"\n- parent: the full parent record (already includes company, customer,\n line items as `items[]`, totals, dates)\n- items: the same line items as a separate array, for clarity\n\nYou do NOT need to re-fetch the parent \u2014 it's already in the message.\nYou DO need to fetch historical data via tools.\n\n# What to look for \u2014 DOCUMENT level (the parent)\n\nUse the tools to find:\n\n1. Duplicates / near-duplicates \u2014 other quotes or orders for the\n same customer in the last 90 days. If >70% of line items match\n the parent, flag as `duplicate`. If 30-70%, flag as `similar`.\n2. Margin \u2014 if the parent's total margin is <15% and the customer's\n typical margin on past orders is >20%, flag `margin_low`.\n3. Anomalies \u2014 any single line item priced >50% above or below the\n historical average for that SKU across the last 20 records.\n4. Cross-company patterns \u2014 same SKU quoted to a different company\n at a much higher price in the last 30 days (could indicate a\n pricing mistake).\n\n# What to look for \u2014 ITEM level (each line item)\n\nFor each line item in the parent, use the tools to find:\n\n1. Match strategy (see \"Matching items without a SKU\" above for\n the full 5-step process). Description search first, SKU only\n as a fallback.\n2. Last quoted sell price + date for this item to ANY customer\n (via `pb_quote_items`, sort by -created, limit 1).\n3. Last actual sold price + date (via `pb_order_items`, sort by\n -created, limit 1).\n4. Average margin % across the last 20 quotes/sales of this item.\n5. Current stock (via `pb_stock`). Status:\n - `in_stock` if qty > 0 and qty >= parent's order qty\n - `low` if qty > 0 but < parent's order qty\n - `out` if qty === 0\n6. All distinct suppliers who have supplied this item\n\n7. Other quotes for this company with the same item \u2014 use the\n `customer_id` or `company_id` from the parent record to find\n OTHER quotes for the same end customer, then search their\n `quote_items` for items with a similar description. Include:\n - quote_number and quote status\n - the sell price and date for comparison\n Example: \"Also quoted to this company on Q010412 (approved 2026-07-10)\n at R344.05 (same item description, different SKU)\"\n, found via\n the price_list matches. See \"Supplier names\" below for how to\n write the name.\n\n# Supplier names\n\n## Supplier ID \u2192 Name resolution from pb_order_items\n\nThe `pb_order_items` collection stores suppliers as PocketBase relation\nIDs (e.g. `rlhxhaw73dxmsu3`). When you query `pb_order_items` and the\nreturned record has a non-null `supplier` field, you MUST look up the\nreadable name via `pb_suppliers` before including it in your output:\n\n filter: id = \"<supplier_id from order_items>\"\n fields: \"id,suppliername,email\"\n\nUse the `suppliername` field from the response as `supplier_name`\nin the output. Do NOT write the raw ID as the name.\n\nExample \u2014 WRONG (raw ID):\n { \"supplier_id\": \"rlhxhaw73dxmsu3\", \"supplier_name\": \"rlhxhaw73dxmsu3\" }\n\nExample \u2014 CORRECT (resolved via pb_suppliers):\n { \"supplier_id\": \"rlhxhaw73dxmsu3\", \"supplier_name\": \"BEARINGS DIRECT\" }\n\n`supplier_id` in the output is OPTIONAL. Only include it if you\nhave one. `supplier_name` MUST be the human-readable name.\n \u2014 come from price_list, NOT from a tool call\n\nThe `supplier` field on every price_list record is the supplier\nNAME directly (e.g. \"DESIGN STUDIO\", \"BEARINGS DIRECT\", \"SKF SA\").\nYou do NOT need a separate `pb_suppliers` tool to resolve names.\n\nWhen the line item has a price_list match, copy the `supplier`\nfield from that record directly into the response's\n`supplier_name`. The dashboard renders the name fine without an id.\n\nExample price_list match:\n { \"id\": \"...\", \"description\": \"...\", \"supplier\": \"DESIGN STUDIO\", ... }\n\nExample output for the suppliers[] entry:\n { \"supplier_name\": \"DESIGN STUDIO\",\n \"last_cost\": 110,\n \"last_bought_date\": \"2026-07-24\",\n \"times_bought\": 1 }\n\n`supplier_id` in the response is OPTIONAL. Only include it if you\nhave one from the line item itself (`quote_items.supplier`) or\nfrom a separate lookup. Most price_list matches give you the name\nwithout an id, and that's fine.\n\n# Output shape \u2014 PARENT\n\n{\n \"summary\": \"<one short sentence, must reference at least one concrete fact from the data, e.g. 'Possible duplicate of Q-1234' or 'No history for Wescoal on this SKU'>\",\n\n \"parent\": {\n \"summary\": \"<1-3 sentence plain-language verdict>\",\n \"warnings\": [\n {\"type\": \"duplicate|similar|margin_low|price_spike|missing_data\", \"severity\": \"high|medium|low\", \"message\": \"<plain text>\", \"ref_id\": \"<pb record id or null>\"}\n ],\n \"suggestions\": [\n {\"message\": \"<plain text>\", \"ref_id\": \"<pb record id or null>\"}\n ]\n }\n}\n\n# Output shape \u2014 ITEMS\n\nFor each line item in the input, return one entry. The minimum is\n`id` + `summary`; everything else is optional. Keep summaries short\n(1-2 sentences) so the model fits all items in the output budget.\n\n{\n \"items\": [\n {\n \"id\": \"<the order_item id from the input>\",\n \"summary\": \"<1-2 sentences about this item \u2014 what you found, what's missing, anything noteworthy>\"\n }\n ]\n}\n\n# Per-item fields \u2014 OPTIONAL\n\nIf you have specific data and it fits the budget, add any of:\n\n last_quoted_price, last_quoted_date,\n last_sold_price, last_sold_date,\n avg_margin_pct,\n in_stock, stock_status (\"in_stock\" | \"low\" | \"out\"),\n is_stock_item,\n suppliers: [{ supplier_name, supplier_id?, last_cost,\n last_bought_date, times_bought }]\n\n`supplier_name` is REQUIRED if you include a suppliers[] entry\n(see \"Supplier names\" above for where it comes from).\n`supplier_id` is OPTIONAL \u2014 only include it if you have one.\n\nDon't try to fill these for items that genuinely have no data \u2014\nomit the key entirely. The UI is shape-agnostic.\n\n# Display rules\n\nWhen writing text for the operator, ALWAYS prefer human-readable\nidentifiers over raw ids:\n\n- For any supplier, use `supplier_name` \u2014 the price_list record's\n `supplier` field IS the name. No lookup needed.\n- For customer/company, use the name from pb_customers /\n pb_companies, not the id.\n- The structured fields (warnings[].ref_id, suppliers[].supplier_id)\n still take raw ids when you have them \u2014 those are join keys, the\n UI uses them to build navigation links. Don't change those.\n- Only the prose / summary / suggestions text gets the friendly names.\n\n\n\n# Sprint categories \u2014 assign every line item to ONE of these\n\nWhen the operator processes this order, each line item will be\nplaced in one of the following sprint categories. Use the category\nname as a plain string in your output. Do NOT create new categories.\n\nAvailable categories:\n{{ SPRINT_CATEGORIES }}\n\nIMPORTANT: Every line item should be assigned a `category` in the\noutput (e.g. \"Hand Tools\" or \"Adhesives & Sealants\"). If the item\ndoes not match any known category, use the closest match or leave\nthe category blank. Never invent a category not in the list above.\n\n# Hard rules (these are non-negotiable)\n\n- Only use ids, prices, dates, and supplier names returned by the\n tools. Never invent.\n- If a tool returned no data for a field, output `null` (numbers\n and dates) or omit the key entirely (suppliers list, warnings list).\n- The `id` field in each `items[]` entry MUST be one of the item ids\n from the input message. The UI uses it to PATCH the right record.\n- The top-level `summary` and `parent.summary` must reference at\n least one concrete fact from the data (a record id, a price, a\n date, a SKU). No generic platitudes like \"This quote looks fine.\"\n- The `items` array MUST contain one entry per line item from the\n input, in the same order. Same id. If the input has 3 items, the\n output MUST have 3 items. No skipping, even if the line item is\n blank. A blank line item with summary \"no data\" is more useful\n to the operator than a missing entry.\n- For a line item with description but no SKU, you MUST query\n pb_price_list with `search_text ~ <keywords>` before declaring\n no history. You MUST attempt at least 2 different keyword\n combinations before giving up. \"No history\" is never an\n acceptable answer for an item with a description.\n- If you give up on a no-SKU item, the item summary must explain\n WHY (e.g. \"no candidate within 5 of 'O19 PPE NOTICE BOARD'\") so\n the operator can fix the data or reword.\n- supplier_name in any suppliers[] array comes from the `supplier`\n field of a price_list match. NOT from <unknown>. NOT from a\n hallucinated name. NEVER write <unknown> in supplier_name.\n If the line item genuinely has no price_list match AND no\n supplier name from any source, omit the entire suppliers[]\n entry for that item \u2014 do not write <unknown>.\n- supplier_id is OPTIONAL in the response. Don't make one up. Only\n include it if you actually have one (from the line item's\n `supplier` field, or fetched via pb_suppliers if you ever need to).\n- The id of a quote, supplier, customer, or company NEVER appears\n in summary, parent.summary, or suggestions[].message text.\n If you need to mention a record, use the human-readable name\n (e.g. quote_number, supplier_name).\n- Output JSON only. No markdown fences. No prose before or after.\n- `ref_id` in warnings/suggestions must be an actual PB record id\n from a tool response. Use `null` if you don't have one.\n\n# Performance\n\n- Use `limit: 1` whenever you only need the most recent record.\n- Use `limit: 20` for averages and cross-quote patterns.\n- Use parallel tool calls in one turn when you have multiple\n independent queries (e.g. for 5 items, query all 5 stocks in\n parallel rather than one at a time).\n- Aim for 10-30 total tool calls per parent. If you're using more\n than 50, you're probably querying per item what you could query\n in bulk.",
|
|
"maxIterations": 25
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
"typeVersion": 3.1,
|
|
"position": [
|
|
4640,
|
|
2032
|
|
],
|
|
"id": "97f8ca28-f2c8-4673-bc43-cb50e9ef2b61",
|
|
"name": "AI Agent1",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "MiniMax-M2.5",
|
|
"options": {
|
|
"maxTokens": 14000
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4160,
|
|
2272
|
|
],
|
|
"id": "a80fd4cc-da32-45f4-8875-a9eb45d1848b",
|
|
"name": "MiniMax Chat Model",
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"schemaType": "manual",
|
|
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"summary\": { \"type\": \"string\" },\n \"parent\": {\n \"type\": \"object\",\n \"properties\": {\n \"summary\": { \"type\": \"string\" },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"type\": { \"type\": \"string\" },\n \"severity\": { \"type\": \"string\" },\n \"message\": { \"type\": \"string\" },\n \"ref_id\": { \"type\": [\"string\", \"null\"] }\n },\n \"additionalProperties\": true\n }\n },\n \"suggestions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"message\": { \"type\": \"string\" },\n \"ref_id\": { \"type\": [\"string\", \"null\"] }\n },\n \"additionalProperties\": true\n }\n }\n },\n \"additionalProperties\": true\n },\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": { \"type\": \"string\" },\n \"summary\": { \"type\": \"string\" },\n \"last_quoted_price\": { \"type\": [\"number\", \"null\"] },\n \"last_quoted_date\": { \"type\": [\"string\", \"null\"] },\n \"last_sold_price\": { \"type\": [\"number\", \"null\"] },\n \"last_sold_date\": { \"type\": [\"string\", \"null\"] },\n \"avg_margin_pct\": { \"type\": [\"number\", \"null\"] },\n \"in_stock\": { \"type\": \"number\" },\n \"stock_status\": { \"type\": \"string\" },\n \"is_stock_item\": { \"type\": \"boolean\" },\n \"suppliers\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"supplier_id\": { \"type\": [\"string\", \"null\"] },\n \"supplier_name\": { \"type\": \"string\" },\n \"last_cost\": { \"type\": \"number\" },\n \"last_bought_date\": { \"type\": \"string\" },\n \"times_bought\": { \"type\": \"integer\" }\n },\n \"additionalProperties\": true\n }\n }\n },\n \"required\": [\"id\", \"summary\"],\n \"additionalProperties\": true\n }\n }\n },\n \"required\": [\"summary\", \"parent\", \"items\"]\n}",
|
|
"autoFix": true
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
5312,
|
|
2272
|
|
],
|
|
"id": "7f92de6d-dbc6-4b02-b681-2ace19fb712d",
|
|
"name": "Structured Output Parser1",
|
|
"retryOnFail": true,
|
|
"waitBetweenTries": 3000
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {
|
|
"maxTokens": 196608
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5392,
|
|
2480
|
|
],
|
|
"id": "bc92b9cc-d942-47ad-b532-96b04d349750",
|
|
"name": "MiniMax Chat Model1",
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.parentId }}",
|
|
"parameters": {
|
|
"allElements": false
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "fb784378-61fe-4930-b295-79e2f36599db",
|
|
"name": "ai_insights",
|
|
"value": "={{ JSON.stringify($json.aiInsights) }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6112,
|
|
1936
|
|
],
|
|
"id": "1f26cda2-a1e0-4241-b6d3-c693a806f991",
|
|
"name": "UPDATE AI ORDER",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
},
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "order_items",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.itemId }}",
|
|
"parameters": {
|
|
"allElements": true
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "91860763-08b8-4738-97b9-738e1e4d1d0c",
|
|
"name": "=ai_item_insights",
|
|
"value": "={{ JSON.stringify($json.aiItemInsights) }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6112,
|
|
2128
|
|
],
|
|
"id": "f360f43c-fa37-4b52-834f-13d09bb8b70f",
|
|
"name": "UPDATE AI ORDER ITEM",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
},
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.kind }}",
|
|
"rightValue": "parent",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
},
|
|
"id": "d07219a2-64b0-4d3c-97ec-7f2b85d2a8c9"
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "5f3c5956-789d-41cc-b284-56080eb9d0a2",
|
|
"leftValue": "={{ $json.kind }}",
|
|
"rightValue": "item",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
5888,
|
|
2032
|
|
],
|
|
"id": "38e87e74-ace7-4483-8a62-cb87c97c797a",
|
|
"name": "Switch",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_suppliers",
|
|
"resource": "suppliers",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4416,
|
|
2272
|
|
],
|
|
"id": "117145a6-3019-42b7-8ae6-d25c813adb42",
|
|
"name": "pb_suppliers",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_price_list",
|
|
"resource": "price_list",
|
|
"parameters": {
|
|
"allElements": false,
|
|
"fields": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Field_Names', ``, 'string') }}",
|
|
"filter": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Filter', ``, 'string') }}"
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4544,
|
|
2272
|
|
],
|
|
"id": "9dfb1316-692b-45f2-8b1f-4fb63a56d8f6",
|
|
"name": "pb_price_list",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_order_items",
|
|
"resource": "order_items",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4672,
|
|
2272
|
|
],
|
|
"id": "4be8f708-b07e-48db-a351-8c04f9621284",
|
|
"name": "pb_order_items",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_stock",
|
|
"resource": "stock_items",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4800,
|
|
2272
|
|
],
|
|
"id": "e5abdc3c-87e8-4281-b773-149ac0b4f065",
|
|
"name": "pb_stock",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_orders",
|
|
"resource": "orders",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4928,
|
|
2272
|
|
],
|
|
"id": "5f1271d5-d0b9-4a9e-b81c-dc83030823b1",
|
|
"name": "pb_orders",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_quote_items",
|
|
"resource": "quote_items",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5056,
|
|
2272
|
|
],
|
|
"id": "b6e1581c-003b-4972-b110-99b380303080",
|
|
"name": "pb_quote_items",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_quotes",
|
|
"resource": "quotes",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5184,
|
|
2272
|
|
],
|
|
"id": "6b123fb9-427f-4886-a739-9e82bed3e187",
|
|
"name": "pb_quotes",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "deepseek-v4-flash",
|
|
"options": {}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4288,
|
|
2272
|
|
],
|
|
"id": "37de04bb-36f3-470f-a22b-117183a00f1b",
|
|
"name": "Ollama Chat Model",
|
|
"credentials": {
|
|
"ollamaApi": {
|
|
"id": "7zP3EPakfo3Tns7J",
|
|
"name": "Ollama account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rule": {
|
|
"interval": [
|
|
{
|
|
"field": "minutes"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.scheduleTrigger",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
-2128,
|
|
1456
|
|
],
|
|
"id": "92ba5d2b-10b6-4ebf-b1e2-360cf05f0d3b",
|
|
"name": "Schedule Trigger"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "a01bff61-3b3a-4fb4-940b-ff8dedd96e73",
|
|
"name": "contact_name",
|
|
"value": "={{ $('AI Agent').item.json.output.order_info.contact_name }}\n",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "34065da3-fc4d-40bf-9032-0ce283a9b892",
|
|
"name": "email_sender",
|
|
"value": "={{ $('AI Agent').item.json.output.order_info.sender_email }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "a467a859-227f-4bbd-83c7-e3ef41660bf4",
|
|
"name": "customer",
|
|
"value": "={{ $('Search customers').item.json?.id || 'xmd68ps32k1x5fu' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "57b896df-4725-4542-bfbc-3308014f5c7e",
|
|
"name": "email_body",
|
|
"value": "={{ ($('Limit').item.json.textContent || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "f45c1fa7-34b4-4994-997e-3747d0e6aecd",
|
|
"name": "email_message_id",
|
|
"value": "={{ $('Limit').item.json.headers['message-id'] }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "cccce8aa-78ea-414e-914f-945194ff9056",
|
|
"name": "description",
|
|
"value": "={{ $('AI Agent').item.json.output.order_items[0].description }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "2546ea57-e8f1-4053-af0d-0c0bba156d57",
|
|
"name": "email_subject",
|
|
"value": "={{ $('Limit').item.json.envelope.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "5ea60f5f-2b1d-4464-a030-1bfd38592a38",
|
|
"name": "order_number",
|
|
"value": "={{ $('AI Agent').item.json.output.order_info.rfq_number || $('Limit').item.json.subject || 'NO-REF' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "7b451b77-966e-46dc-aafe-f03478182998",
|
|
"name": "company",
|
|
"value": "={{ $('Search customers').item.json?.company || 'fufdg0k9q128wzv' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "b248cb18-bd40-4dfc-adb2-7b64f3f987f3",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "b253f84b-7286-4e5f-a9b0-ec0c770c2684",
|
|
"name": "rep",
|
|
"value": "={{ $json.rep }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "65376803-9e69-4f6f-996e-50875fe659af",
|
|
"name": "Create orders",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3024,
|
|
1952
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "downloadAttachment",
|
|
"mailboxPath": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $json.uid }}",
|
|
"allAttachments": true,
|
|
"includeInlineAttachments": true
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-208,
|
|
2416
|
|
],
|
|
"id": "29804fcc-7dd3-4cfe-a7b3-1a3470acbd86",
|
|
"name": "Download ATTACHMENT",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"pageRange": "="
|
|
},
|
|
"type": "@bitovi/n8n-nodes-pdf-to-png.pdfToPng",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
912,
|
|
1856
|
|
],
|
|
"id": "3757a091-bcf3-4a3a-93ac-7de1c458561c",
|
|
"name": "PDF to PNG"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "c7872cb1-6bcc-4021-9c0b-3d90b337375a",
|
|
"leftValue": "={{ $json.text }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
688,
|
|
1936
|
|
],
|
|
"id": "b625acdd-e061-4a07-a230-2d975010969d",
|
|
"name": "If3"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "image",
|
|
"modelId": {
|
|
"__rl": true,
|
|
"value": "MiniMax-M3",
|
|
"mode": "list",
|
|
"cachedResultName": "MiniMax-M3"
|
|
},
|
|
"text": "=What's in this image? if its more than one order or image can you combinde thye contents into 1 and just add the order number next to the items on the order that it refrences to ",
|
|
"inputType": "binary",
|
|
"options": {
|
|
"maxTokens": 524288
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.anthropic",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1136,
|
|
1856
|
|
],
|
|
"id": "af384a3c-5189-4cd9-a472-0d113158dfee",
|
|
"name": "Analyze image",
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "jLsa9I3U2W6vqzeE",
|
|
"name": "Anthropic account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"formBinaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3920,
|
|
1424
|
|
],
|
|
"id": "3fd765c7-32e5-4240-8206-59d7d16919b6",
|
|
"name": "Update orders",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Combine All Extracted Texts\"\nconst items = $input.all();\n\n// Extract text from each item (handles both nested content arrays and flat text)\nconst allTexts = items.map(item => {\n if (item.json.content && item.json.content[0]?.text) {\n return item.json.content[0].text;\n }\n return item.json.text || '';\n}).filter(Boolean);\n\n// Combine them into one single string with a clean separator\nreturn [{\n json: {\n aggregatedText: allTexts.join('\\n\\n--- [ NEXT ATTACHMENT ] ---\\n\\n')\n }\n}];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1728,
|
|
2624
|
|
],
|
|
"id": "fdb4419c-7fc8-42d8-ba55-0e2e4a732786",
|
|
"name": "Code in JavaScript4"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "moveEmail",
|
|
"sourceMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $('Limit').first().json.uid }}",
|
|
"destinationMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Completed",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Completed"
|
|
}
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6560,
|
|
1664
|
|
],
|
|
"id": "c82d9996-f8e5-4437-80fd-74b24390bac4",
|
|
"name": "MoveEmail email2",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "setEmailFlags",
|
|
"mailboxPath": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Completed",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Completed"
|
|
},
|
|
"emailUid": "={{ $('Limit').first().json.uid }}",
|
|
"flags": {
|
|
"\\Seen": true
|
|
}
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6784,
|
|
1664
|
|
],
|
|
"id": "ade33045-d40e-4800-b65a-ec345b2e25ad",
|
|
"name": "SetEmailFlags email",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "moveEmail",
|
|
"sourceMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX",
|
|
"cachedResultUrl": ""
|
|
},
|
|
"emailUid": "={{ $json.attributes?.uid || $json.uid }}",
|
|
"destinationMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Bounce&Supplier",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Bounce&Supplier"
|
|
}
|
|
},
|
|
"id": "3293f87c-4e33-40a2-845f-914cc8b3bc05",
|
|
"name": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-208,
|
|
480
|
|
],
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"fromEmail": "ORDERS@WFSALES.CO.ZA",
|
|
"toEmail": "CONAR@WFSALES.CO.ZA",
|
|
"subject": "CHECK THIS BOUNCE OR SUPPLIER MAIL",
|
|
"html": "={{ $json.envelope.from[0].name }}\n{{ $json.envelope.from[0].address }}\n\n{{ $json.htmlContent }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.emailSend",
|
|
"typeVersion": 2.1,
|
|
"position": [
|
|
-432,
|
|
416
|
|
],
|
|
"id": "2e4e5b0c-706c-442e-9b63-853b47581a12",
|
|
"name": "Send an Email",
|
|
"webhookId": "f2120f43-da70-4fea-99b1-f0f53487e7f1",
|
|
"credentials": {
|
|
"smtp": {
|
|
"id": "qnkWLVAGkq9H7jT5",
|
|
"name": "PO_WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// EDIT: add supplier domains to this list, or swap for a live\n// pb_suppliers lookup later\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst supplierDomains = ['festo.com'];\n\n// PO number pattern: common South African / industrial formats\n// Matches things like ZA10PO26010307, PO-2026-08-014, PO2026001, etc.\nconst PO_PATTERN = /\\b(?:PO[-\\s]?\\d{4,}|ZA\\d+PO\\d+)\\b/gi;\n\nconst items = $input.all();\n\nreturn items.map(item => {\n const j = item.json;\n const sender = (j.envelope?.sender?.[0]?.address || j.from?.text || '').toLowerCase();\n const subject = (j.envelope?.subject || j.subject || '').toLowerCase();\n const bodyText = (j.text || j.textPlain || j.body || '').toString();\n const headers = j.headers || {};\n\n const contentType = (headers['content-type'] || headers['Content-Type'] || '').toString().toLowerCase();\n const autoSubmittedHeader= (headers['auto-submitted'] || headers['Auto-Submitted'] || '').toString().toLowerCase();\n const precedence = (headers['precedence'] || headers['Precedence'] || '').toString().toLowerCase();\n\n // \u2500\u2500 1. CONFIRMED BOUNCE \u2014 RFC 3464 delivery-status report \u2500\u2500\n const isConfirmedBounce = contentType.includes('report-type=delivery-status');\n\n // \u2500\u2500 2. CONFIRMED SUPPLIER \u2014 domain match \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const senderDomain = (sender.split('@')[1] || '');\n const isConfirmedSupplier = !isConfirmedBounce &&\n supplierDomains.some(d => senderDomain.endsWith(d));\n\n // \u2500\u2500 3. MULTI-ORDER \u2014 multiple PDFs OR multiple PO numbers \u2500\u2500\n // Count PDF attachments\n const attachments = Array.isArray(j.attachments) ? j.attachments : [];\n const pdfCount = attachments.filter(a =>\n (a.contentType || a.type || '').toLowerCase().includes('pdf')\n ).length;\n\n // Count distinct PO numbers across subject + body\n const subjectPOs = (subject.match(PO_PATTERN) || []);\n const bodyPOs = (bodyText.match(PO_PATTERN) || []);\n const allPOs = [...new Set([...subjectPOs, ...bodyPOs].map(p => p.toUpperCase()))];\n\n const isMultiOrder = !isConfirmedBounce && !isConfirmedSupplier &&\n (pdfCount > 1 || allPOs.length > 1);\n\n // \u2500\u2500 4. WEAK SIGNALS \u2014 hand off to AI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const weakBounceSignal =\n sender.includes('mailer-daemon') ||\n sender.includes('postmaster') ||\n subject.includes('delivery failed') ||\n subject.includes('undeliverable') ||\n subject.includes('returning message to sender') ||\n subject.includes('out of office') ||\n subject.includes('automatic reply') ||\n (autoSubmittedHeader && autoSubmittedHeader !== 'no') ||\n precedence.includes('bulk') || precedence.includes('auto_reply');\n\n // \u2500\u2500 ROUTE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let route = 'customer';\n if (isConfirmedBounce) route = 'bounce';\n else if (isConfirmedSupplier) route = 'supplier';\n else if (isMultiOrder) route = 'multi_order';\n else if (weakBounceSignal) route = 'uncertain';\n\n return { json: { ...j, route, _debug: { pdfCount, allPOs } } };\n});"
|
|
},
|
|
"id": "dab6ee17-4d4b-4278-9d41-f59c9a281bfc",
|
|
"name": "Classify Email (Deterministic)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1680,
|
|
1456
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "r-bounce",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "bounce",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "r-supplier",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "supplier",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "r-multi",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "multi_order",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "r-uncertain",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "uncertain",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "bc725194-0ec7-4128-a5e2-849ee1f35320",
|
|
"name": "Route Email",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
-1456,
|
|
1408
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "MiniMax-M2.1",
|
|
"options": {
|
|
"maxTokens": 10,
|
|
"temperature": 0.1,
|
|
"timeout": 30000,
|
|
"maxRetries": 2
|
|
}
|
|
},
|
|
"id": "9e6ff244-47a2-415e-b669-90206a8a16a4",
|
|
"name": "Cheap Classifier Model (Minimax)",
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1168,
|
|
2400
|
|
],
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"promptType": "define",
|
|
"text": "=Sender: {{ $json.envelope?.sender?.[0]?.address || $json.from?.text }}\nSubject: {{ $json.envelope?.subject || $json.subject }}\nBody snippet: {{ ($json.text || $json.textPlain || '').toString().substring(0,300) }}",
|
|
"messages": {
|
|
"messageValues": [
|
|
{
|
|
"message": "You classify inbound emails for a company's sales quotes inbox. Reply with ONLY one word, no punctuation: BOUNCE, SUPPLIER, or CUSTOMER.\nBOUNCE = an automated delivery failure, out-of-office, or auto-reply notice \u2014 not a real message from a person.\nSUPPLIER = a message from one of our own suppliers (order confirmations, invoices, stock/backorder updates) \u2014 not a customer request.\nCUSTOMER = anything else, including quote requests, replies to quotes, or order queries from a customer."
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"id": "bc3180fe-ea37-42f2-b934-e1857b7b0ffb",
|
|
"name": "AI Classify Email (cheap)",
|
|
"type": "@n8n/n8n-nodes-langchain.chainLlm",
|
|
"typeVersion": 1.5,
|
|
"position": [
|
|
-1232,
|
|
2176
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const items = $input.all();\nreturn items.map(item => {\n const raw = (item.json.text || item.json.output || item.json.response || '').toString().trim().toUpperCase();\n let route = 'customer';\n if (raw.includes('BOUNCE')) route = 'bounce';\n else if (raw.includes('SUPPLIER')) route = 'supplier';\n return { json: { ...item.json, route } };\n});"
|
|
},
|
|
"id": "18fd69e8-8b3d-4f80-8feb-d7dd715e3d44",
|
|
"name": "Parse AI Classification",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-880,
|
|
2176
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "ai-bounce",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "bounce",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "ai-supplier",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "supplier",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "c0293379-2505-438d-bac9-1796c48f737b",
|
|
"name": "Route Email (AI Result)",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
-656,
|
|
2160
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "downloadAttachment",
|
|
"mailboxPath": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $json.uid }}",
|
|
"allAttachments": true,
|
|
"includeInlineAttachments": true
|
|
},
|
|
"id": "0a0715c2-1bbc-42c6-ba58-04a2bcfc461b",
|
|
"name": "Download ATTACHMENT (Multi)",
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1168,
|
|
1456
|
|
],
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const item = $input.first();\nconst meta = $('CHECK FOR NEW MAIL').item.json;\nconst subject = meta.envelope?.subject || meta.subject || \"\";\nconst bodyText = (meta.textContent || meta.text || \"\").toString();\nconst binary = item.binary || {};\n\n// Recover the real customer if this looks like an internal forward (e.g. laura@wfsales.co.za forwarding a Master Drilling PO)\nlet fromEmail = meta.envelope?.sender?.[0]?.address || \"\";\nlet fromName = meta.envelope?.sender?.[0]?.name || \"\";\nif (/@wfsales\\.co\\.za$/i.test(fromEmail)) {\n const m = bodyText.match(/From:\\s*([^<\\n]+)<([^>]+)>/i);\n if (m) {\n fromName = m[1].trim();\n fromEmail = m[2].trim();\n }\n}\nconst toEmail = meta.envelope?.to?.[0]?.address || \"\";\nconst messageId = meta.headers?.[\"message-id\"] || meta.headers?.[\"Message-Id\"] || \"\";\n\n// Same PO pattern as \"Classify Email (Deterministic)\" so detection stays consistent\nconst PO_PATTERN = /\\b(?:PO[-\\s]?\\d{4,}|ZA\\d+PO\\d+)\\b/gi;\nconst subjectPOs = subject.match(PO_PATTERN) || [];\nconst bodyPOs = bodyText.match(PO_PATTERN) || [];\nconst poNumbers = [...new Set([...subjectPOs, ...bodyPOs].map(p => p.toUpperCase().replace(/\\s+/g, \"\")))];\n\nif (poNumbers.length === 0) {\n return [{\n json: {\n needs_review: true,\n reason: \"no PO numbers found in subject/body despite multi_order routing\",\n subject,\n from_email: fromEmail,\n from_name: fromName,\n to_email: toEmail,\n body_text: bodyText,\n message_id: messageId,\n },\n binary,\n }];\n}\n\n// Group attachments to the PO number(s) their filename mentions\nconst groups = {};\nfor (const po of poNumbers) groups[po] = {};\nconst unmatched = [];\n\nfor (const key of Object.keys(binary)) {\n const bin = binary[key];\n const fname = (bin.fileName || \"\").toUpperCase().replace(/\\s+/g, \"\");\n const matchedPo = poNumbers.find(po => fname.includes(po));\n if (matchedPo) {\n groups[matchedPo][key] = bin;\n } else {\n unmatched.push(key);\n }\n}\n\nreturn poNumbers.map(po => ({\n json: {\n po_number: po,\n subject: `${po} \\u2014 ${subject}`,\n from_email: fromEmail,\n from_name: fromName,\n to_email: toEmail,\n body_text: bodyText,\n message_id: `${messageId}-${po}`,\n needs_review: false,\n },\n binary: groups[po],\n}));"
|
|
},
|
|
"id": "fa6b7c31-8e7b-4b03-a68a-69b41d2eb6d3",
|
|
"name": "Group attachments by PO",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-880,
|
|
1456
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "9e8b5d6e-ea11-414c-b4d2-7035ad6ed96a",
|
|
"leftValue": "={{ $json.needs_review }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "notEquals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "579b92e8-f5fc-4a76-8f04-03e9e56a1a95",
|
|
"name": "Has valid orders?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
-656,
|
|
1456
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "dbfb84ab-b929-486c-9487-3aec267e78b0",
|
|
"name": "Per-order loop",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
-432,
|
|
1312
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const item = $input.first();\nconst j = item.json;\nreturn [{\n json: {\n po_number: j.po_number,\n subject: j.subject,\n text: j.body_text,\n textContent: j.body_text,\n envelope: {\n subject: j.subject,\n sender: [{ address: j.from_email, name: j.from_name }],\n to: [{ address: j.to_email }],\n },\n headers: { \"message-id\": j.message_id },\n },\n binary: item.binary,\n}];"
|
|
},
|
|
"id": "f956ad2a-2034-4f17-bce4-f37ceeaaddff",
|
|
"name": "Limit (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-208,
|
|
704
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Turn one order item with N binary attachments into N items,\n// each carrying exactly one binary, plus the original order json.\nconst item = $input.first();\nconst binaryKeys = Object.keys(item.binary || {});\n\nif (binaryKeys.length === 0) {\n return [{ json: { ...item.json, hasAttachments: false, totalAttachments: 0 }, binary: {} }];\n}\n\nreturn binaryKeys.map((key, index) => ({\n json: {\n ...item.json,\n binaryKey: key,\n fileName: item.binary[key].fileName,\n mimeType: item.binary[key].mimeType,\n fileSize: item.binary[key].fileSize,\n attachmentIndex: index,\n totalAttachments: binaryKeys.length,\n hasAttachments: true\n },\n binary: { [key]: item.binary[key] }\n}));"
|
|
},
|
|
"id": "bf5deb63-1259-45ff-82d6-d6828712de29",
|
|
"name": "Split Attachments1 (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
16,
|
|
704
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "application/pdf",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
},
|
|
"id": "b737fbd9-7eec-4df0-b168-bbb410c89993"
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "db298d9c-f9cd-4074-8668-c2e04ffe269c",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/csv",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "1d14db84-b83f-49d6-ac5d-f5f6e073fa4f",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/xls",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "65b2d52e-1965-4004-b244-9b815b4a2588",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/xlsx",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "ccc71a40-1399-441c-b166-9c2e19b1d723",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "=text/text",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "e1f86572-8d48-4e4b-beea-ab50c8aeaaaa",
|
|
"leftValue": "={{ $('Limit (Multi)').item.json.text || $('Limit (Multi)').item.json.textContent }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "f22c5556-91e4-416b-a003-3fe4ee573a36",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "image/png",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "393b3c7a-1aa7-48f8-bae7-70018b7e8fc0",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "image/jpeg",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "8e6cc1e9-7469-4ba9-8035-84f8d684add4",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "application/octet-stream",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"allMatchingOutputs": true
|
|
}
|
|
},
|
|
"id": "40d530e9-48d0-4cf4-a8cd-5aba8fb46e87",
|
|
"name": "Switch1 (Multi)",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
240,
|
|
592
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "pdf",
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"options": {
|
|
"joinPages": true,
|
|
"keepSource": "both"
|
|
}
|
|
},
|
|
"id": "2ecc632f-7099-42fd-8016-8074f4eeda06",
|
|
"name": "Extract PDF (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
464,
|
|
128
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "b4efd17b-967f-4c5a-957a-5f4c93a71c94",
|
|
"leftValue": "={{ $json.text }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "58ba8e39-80b7-41d2-b1f8-efac911a3e36",
|
|
"name": "If3 (Multi)",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
688,
|
|
128
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"pageRange": "="
|
|
},
|
|
"id": "1eaf6227-00bd-4b22-9239-77ed2837cfdf",
|
|
"name": "PDF to PNG (Multi)",
|
|
"type": "@bitovi/n8n-nodes-pdf-to-png.pdfToPng",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
912,
|
|
48
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "image",
|
|
"modelId": {
|
|
"__rl": true,
|
|
"value": "MiniMax-M3",
|
|
"mode": "list",
|
|
"cachedResultName": "MiniMax-M3"
|
|
},
|
|
"text": "What's in this image? if its more than one order or image can you combinde thye contents into 1 and just add the order number next to the items on the order that it refrences to ",
|
|
"inputType": "binary",
|
|
"options": {
|
|
"maxTokens": 524288
|
|
}
|
|
},
|
|
"id": "9cf4c349-bb57-4d0d-aa3b-b42bb4322a97",
|
|
"name": "Analyze image (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.anthropic",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1136,
|
|
48
|
|
],
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "jLsa9I3U2W6vqzeE",
|
|
"name": "Anthropic account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "517a879c-bfd8-4a6f-b29f-74db165d62b0",
|
|
"name": "Extract from CSV1 (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
336
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xls",
|
|
"options": {}
|
|
},
|
|
"id": "b612b79b-8723-4cc5-a5d2-dd43e50564fe",
|
|
"name": "Extract from XLS1 (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
528
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xlsx",
|
|
"options": {}
|
|
},
|
|
"id": "0e932fde-f4f6-4e27-93e8-d21001207f27",
|
|
"name": "Extract from XLSX1 (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
720
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "text",
|
|
"options": {}
|
|
},
|
|
"id": "f4e8bdf3-5811-4b84-8234-02a1ca995fc2",
|
|
"name": "Extract from TXT1 (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
912
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "image",
|
|
"modelId": {
|
|
"__rl": true,
|
|
"value": "MiniMax-M3",
|
|
"mode": "list",
|
|
"cachedResultName": "MiniMax-M3"
|
|
},
|
|
"inputType": "binary",
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"options": {}
|
|
},
|
|
"id": "6d01cb20-d46c-47b3-bd1a-b3bfc6ece878",
|
|
"name": "Analyze image1 (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.anthropic",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
912,
|
|
1200
|
|
],
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "jLsa9I3U2W6vqzeE",
|
|
"name": "Anthropic account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Normalize Image Text\"\nconst item = $input.first();\nconst extractedText = item.json.content?.[0]?.text || '';\nreturn [{\n json: {\n ...item.json,\n text: extractedText\n }\n}];"
|
|
},
|
|
"id": "c1df2c45-a49b-4988-a800-12aa3f67064f",
|
|
"name": "Code in JavaScript3 (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1136,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Combine All Extracted Texts\"\nconst items = $input.all();\n\nconst allTexts = items.map(item => {\n let t;\n if (item.json.content && item.json.content[0]?.text) {\n t = item.json.content[0].text;\n } else {\n t = item.json.text || '';\n }\n // Strip stray curly braces (common in free-form AI vision output) so they\n // don't break n8n's {{ }} expression parsing further downstream.\n return t.replace(/[{}]/g, '');\n}).filter(Boolean);\n\nreturn [{\n json: {\n aggregatedText: allTexts.join('\\n\\n--- [ NEXT ATTACHMENT ] ---\\n\\n')\n }\n}];"
|
|
},
|
|
"id": "e982562d-b370-4dc1-9a6a-b065a25290f4",
|
|
"name": "Code in JavaScript4 (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1360,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"promptType": "define",
|
|
"text": "Email Subject: {{ $('Limit (Multi)').item.json.envelope.subject }}\\n\\nSender Email: {{ $('Limit (Multi)').item.json.envelope.sender[0].address }}\\n\\nRecipient Email: {{ $('Limit (Multi)').item.json.envelope.to[0].address }}\\n\\n--- DETERMINISTIC CONTACT NAME (pre-extracted) ---\\n{{ $('Extract contact name (deterministic) (Multi)').item.json.contact_name || '(none \u2014 extract from the documents below)' }}\\n\\n--- EMAIL BODY ---\\n{{ $('Limit (Multi)').item.json.textContent }}\\n\\n\\n--- ORDER DETAILS ---\\n\\n\\n\\n--- ATTACHMENT TEXT (ALL PDFs COMBINED) ---\\n{{ $('Code in JavaScript4 (Multi)').item.json.aggregatedText }}\\n\\n\\n",
|
|
"hasOutputParser": true,
|
|
"options": {
|
|
"systemMessage": "You are an expert procurement and data extraction AI. Your task is to process incoming orders by analyzing both the email body and any attached document text.\n\nYou must reconcile the information. For example, if the email says \"Please quote the attached 5 items,\" you must extract the company details from the email signature and the 5 items from the attachment text.\n\nExtract the data and return it STRICTLY as a valid JSON object matching the exact structure below. Do not include markdown formatting (like ```json), conversational text, or explanations. Only return the JSON.\n\nJSON STRUCTURE:\n{\n \"order_info\": {\n \"company_name\": \"Extract the buyer/end-customer company name.\",\n \"contact_name\": \"Extract the actual End-User or Requester of the goods (Look for fields like 'Requester', 'Deliver To', 'Attention', or names in the Notes). Do NOT use the 'Buyer' or 'Created By' name if a Requester is listed.\",\n \"sender_email\": \"Extract the email address of the person requesting the quote.\",\n \"rfq_number\": \"Extract any visible RFQ, Reference, or Order number provided by the customer. Leave empty if none.\",\n \"notes\": \"Summarize any specific instructions, delivery requirements, or context from the email body (e.g., 'Urgent delivery required').\"\n },\n \"order_items\": [\n {\n \"description\": \"The exact name, SKU, or description of the requested item.\",\n \"quantity\": 1,\n \"unit_price\": 0.00,\n \"notes\": \"Any specific unit of measure (e.g., 'meters', 'kg') or specific notes for this single item.\"\n }\n ]\n}\n\nRULES:\n1. Do not invent items, prices, or quantities.\n2. If there are no line items found in either the email or the attachment, return an empty array for \"order_items\".\n3. Ensure numerical quantities and unit_prices are returned as clean numbers, not strings. Do not include currency symbols (e.g., return 660, not \"R660\" or \"ZAR 660\"). If a unit price is not provided, return 0.\n4. CUSTOMER EXTRACTION PRIORITY: You must look for the \"company_name\" and \"contact_name\" inside the ATTACHMENT TEXT first (e.g., the letterhead or \"Bill To\" section of the Purchase Order). Use the EMAIL BODY (e.g., email signature or instructions) as a fallback ONLY if these details are completely missing from the attachments.\n5. REQUESTER VS BUYER: Large corporate orders often list a Procurement Buyer and a specific Requester. Prefer the Requester as contact_name when both are present.\\n\\n7. CONTACT_NAME PRIORITY: If the 'DETERMINISTIC CONTACT NAME' section above contains a non-empty name (anything that is NOT '(none \u2014 extract from the documents below)'), use that value verbatim as `order_info.contact_name`. The deterministic extractor is more reliable than the LLM for the contact name (see the audit report). Only fall back to extracting from the email signature or PDF Requester/Deliver To/Attention lines if the deterministic name is missing.\\n8. EMPTY WHEN UNCERTAIN: If you cannot find a contact name ANYWHERE (neither in the deterministic section, nor the email signature, nor the PO requester line), return `contact_name: \"\"` \u2014 do NOT return placeholder strings like 'Unknown', 'Not specified', 'N/A', or the company name. Placeholder values are rejected by the operator UI and create duplicate customer matches.\\n9. STRIP WHITESPACE: If you do extract a name, return it with no trailing newline, no leading whitespace, and no trailing whitespace. The DB stores the value verbatim; stray whitespace renders as a dangling separator dot on the orders list.\\n"
|
|
}
|
|
},
|
|
"id": "db98c936-7ce4-4d17-a932-b6c481c9262a",
|
|
"name": "AI Agent (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
"typeVersion": 2.2,
|
|
"position": [
|
|
1616,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {
|
|
"hideThinking": true,
|
|
"temperature": 0.1,
|
|
"timeout": 100000,
|
|
"maxRetries": 3
|
|
}
|
|
},
|
|
"id": "155e8f21-9b7f-4911-b129-45554c6159ad",
|
|
"name": "Minimax Chat Model (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1600,
|
|
1024
|
|
],
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsonSchemaExample": "{\n \"order_info\": {\n \"company_name\": \"Harmony\",\n \"contact_name\": \"Mark Wilkie\",\n \"sender_email\": \"davidels1986@gmail.com\",\n \"rfq_number\": \"PO-20260604-004\",\n \"notes\": \"Please quote on Isherwood letterhead\"\n },\n \"order_items\": [\n {\n \"description\": \"TURBO END+ SUPER TOUGH COUNTERSINK 14-60MM\",\n \"quantity\": 2,\n \"unit_price\": 0.00,\n \"notes\": \"SKU: JEIETCSK-60\"\n }\n ]\n}",
|
|
"autoFix": true
|
|
},
|
|
"id": "17a486c2-4100-4f25-aa57-642f828f7a0b",
|
|
"name": "Structured Output Parser (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
1728,
|
|
1024
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "f901dce1-4cf0-4e4c-be76-2177b8b3e517",
|
|
"leftValue": "={{ $json.output.order_items.length }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEquals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {}
|
|
},
|
|
"id": "d1db0951-dd17-4851-85ff-c5722f20f635",
|
|
"name": "If1 (Multi)",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
2224,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "customers",
|
|
"parameters": {
|
|
"elementsPerPage": 12,
|
|
"filter": "={{ \n (() => {\n const name = $('AI Agent (Multi)').item.json.output.order_info.contact_name || '';\n const email = $('AI Agent (Multi)').item.json.output.order_info.order_info?.sender_email || $('AI Agent (Multi)').item.json.output.order_info.sender_email || '';\n \n const nameKeywords = name.replace(/mr\\.|mrs\\.|ms\\.|mr|mrs|ms/gi, '').split(/[\\s,]+/).filter(w => w.length > 2);\n const nameQuery = nameKeywords.length > 0 ? nameKeywords.map(w => `customer ~ '${w}'`).join(' && ') : '';\n \n const emailQuery = email ? `email ~ '${email}'` : '';\n \n if (!nameQuery && !emailQuery) {\n return \"id = 'xmd68ps32k1x5fu'\";\n }\n \n return [nameQuery, emailQuery].filter(Boolean).map(q => `(${q})`).join(' || ');\n })()\n}}"
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "ae289c8f-b649-424b-be87-d422815642a2",
|
|
"name": "Search customers (Multi)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2576,
|
|
688
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "9a68ceb0-89fa-43ea-9b38-238b9c9cb02c",
|
|
"name": "contact_name",
|
|
"value": "={{ $('AI Agent (Multi)').item.json.output.order_info.contact_name }}\n",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "c6190435-249d-4150-a435-f0a3ed9a4a26",
|
|
"name": "email_sender",
|
|
"value": "={{ $('AI Agent (Multi)').item.json.output.order_info.sender_email }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "7bb6d032-2683-44b6-80a9-18c6fce371ff",
|
|
"name": "customer",
|
|
"value": "={{ $('Search customers (Multi)').item.json?.id || 'xmd68ps32k1x5fu' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "f6772599-5d9f-41e1-a08c-a4bbce5eff26",
|
|
"name": "email_body",
|
|
"value": "={{ ($('Limit (Multi)').item.json.textContent || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "fe222f30-e88d-4284-a025-c989f31dfaca",
|
|
"name": "email_message_id",
|
|
"value": "={{ $('Limit (Multi)').item.json.headers['message-id'] }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "5353b98b-e37a-4cc0-a394-2be8396615fb",
|
|
"name": "description",
|
|
"value": "={{ $('AI Agent (Multi)').item.json.output.order_items[0].description }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "d694b5ac-323c-4935-8f4e-6e39a828daa3",
|
|
"name": "email_subject",
|
|
"value": "={{ $('Limit (Multi)').item.json.envelope.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "681a3e58-2a33-455a-906a-1ced89049823",
|
|
"name": "order_number",
|
|
"value": "={{ $('AI Agent (Multi)').item.json.output.order_info.rfq_number || $('Limit (Multi)').item.json.po_number || 'NO-REF' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "a8be8f38-a32e-4bae-92dd-8e0fcc3b3c63",
|
|
"name": "company",
|
|
"value": "={{ $('Search customers (Multi)').item.json?.company || 'fufdg0k9q128wzv' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "88728789-c85c-49f6-81c5-2d6782ebe4f5",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "bf66a8fe-a4e3-4787-a51f-44700ce18097",
|
|
"name": "Create orders (Multi)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2800,
|
|
688
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Split Order Items\"\nconst orderId = $('Create orders (Multi)').item.json.id;\nconst items = $('AI Agent (Multi)').item.json.output.order_items;\n\nreturn items.map(item => ({\n json: {\n order: orderId,\n description: item.description,\n quantity: item.quantity,\n selling_price: item.unit_price,\n notes: item.notes\n }\n}));\n"
|
|
},
|
|
"id": "3c8f445d-80b5-4d88-85af-901cd213e0ce",
|
|
"name": "Code in JavaScript2 (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3024,
|
|
288
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "order_items",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "order-1",
|
|
"name": "order",
|
|
"value": "={{ $json.order }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "description-1",
|
|
"name": "description",
|
|
"value": "={{ $json.description }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "quantity-1",
|
|
"name": "quantity",
|
|
"value": "={{ $json.quantity }}",
|
|
"type": "number"
|
|
},
|
|
{
|
|
"id": "notes-1",
|
|
"name": "notes",
|
|
"value": "={{ $json.notes }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "33ce0e0d-b1ab-4683-9b1e-7c8322012372",
|
|
"name": "item_status",
|
|
"value": "pending",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "003e4156-642d-439f-9bc3-50e1ee50a77e",
|
|
"name": "selling_price",
|
|
"value": "={{ $json.selling_price }}",
|
|
"type": "number"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "1fd4e23b-82f4-49bd-a0a5-31a0cd490d6d",
|
|
"name": "Create order_items1 (Multi)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3248,
|
|
288
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"binaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"id": "74379390-d7d0-4303-863b-1876ff4f8c3b",
|
|
"name": "Update orders (Multi)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3696,
|
|
960
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"mode": "chooseBranch",
|
|
"useDataOfInput": "=1"
|
|
},
|
|
"id": "bfb6f54a-0156-481a-a26b-7bb04abb434a",
|
|
"name": "Merge order completion",
|
|
"type": "n8n-nodes-base.merge",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
3472,
|
|
336
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "8ff90d7c-7f64-4b83-ad10-e5f5c14dce31",
|
|
"name": "contact_name",
|
|
"value": "={{ $json.from_name || 'Unknown - needs review' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "9ad419b0-56e5-465b-b234-e2a688fd1c2b",
|
|
"name": "email_sender",
|
|
"value": "={{ $json.from_email }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "a7aa7543-62dc-4a3e-a2f0-8b0c37b55def",
|
|
"name": "customer",
|
|
"value": "xmd68ps32k1x5fu",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "fdb069b1-62ce-4383-b058-9efac4757386",
|
|
"name": "email_body",
|
|
"value": "={{ ($json.body_text || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "2b371925-94d9-4bf2-a069-0f5eef6b644a",
|
|
"name": "email_message_id",
|
|
"value": "={{ $json.message_id }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "3bdda609-6421-4076-9c26-ed2119fba09e",
|
|
"name": "email_subject",
|
|
"value": "={{ $json.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "15f1fa2e-6cae-457b-b712-a7e851cd3293",
|
|
"name": "order_number",
|
|
"value": "NO-REF",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "c438ce77-4241-459e-ac9e-6a341a4e23f8",
|
|
"name": "company",
|
|
"value": "fufdg0k9q128wzv",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "344320aa-d757-49f1-8b85-79345ed0627d",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "f09a7a75-7782-4d21-bc49-aca455ed41ca",
|
|
"name": "description",
|
|
"value": "=[NEEDS REVIEW] AUTO-FLAGGED: multi-order routing fired but no PO number could be found in the subject/body. Raw email body is stored in email_body \u2014 needs manual entry.",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "62ca378a-d7c7-4a94-87ee-88a846dd556a",
|
|
"name": "Create order (no PO detected - raw)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-432,
|
|
1984
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"formBinaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"id": "94229c67-a226-4fce-9fe8-3cebc4f5563f",
|
|
"name": "Update order (attach raw file - whole email)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
240,
|
|
1984
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "cbb4454a-969d-43c1-86ca-65f4088a260f",
|
|
"name": "contact_name",
|
|
"value": "={{ $('Limit (Multi)').item.json.envelope.sender[0].name || 'Unknown - needs review' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "fb260728-17a0-4812-9773-a205cabbbfec",
|
|
"name": "email_sender",
|
|
"value": "={{ $('Limit (Multi)').item.json.envelope.sender[0].address }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "d55bace2-1c98-47e8-a951-633e9c0b2b86",
|
|
"name": "customer",
|
|
"value": "xmd68ps32k1x5fu",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "5e16996d-334d-4765-b592-2cb863afcb83",
|
|
"name": "email_body",
|
|
"value": "={{ ($('Limit (Multi)').item.json.textContent || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "66ce98be-15fb-4c4c-9780-878a0976324d",
|
|
"name": "email_message_id",
|
|
"value": "={{ $('Limit (Multi)').item.json.headers['message-id'] }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "69b8c4a9-361f-4e74-91c4-74ed962a67de",
|
|
"name": "email_subject",
|
|
"value": "={{ $('Limit (Multi)').item.json.envelope.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "6b4c244c-859e-4c7b-984e-410cf9b8a8ce",
|
|
"name": "order_number",
|
|
"value": "={{ $('Limit (Multi)').item.json.po_number || 'NO-REF' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "01a2b13f-07af-4352-9eea-6e23f5601455",
|
|
"name": "company",
|
|
"value": "fufdg0k9q128wzv",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "a0ad6940-50b9-45e3-b264-a04da3d81ecd",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "b079044e-ab28-46d1-9eda-696708200eeb",
|
|
"name": "description",
|
|
"value": "=[NEEDS REVIEW] AUTO-FLAGGED: AI Agent could not extract any line items for this PO. Raw extracted text: {{ ($('Code in JavaScript4 (Multi)').item.json.aggregatedText || '').substring(0, 2000) }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "2a203a70-6b63-4a9e-bcbf-58675401d5a5",
|
|
"name": "Create order (no items extracted - raw)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2576,
|
|
1152
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"formBinaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"id": "254a4383-97fc-43b2-aae0-45b5bccef54d",
|
|
"name": "Update order (attach raw file - per order)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3248,
|
|
1152
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "moveEmail",
|
|
"sourceMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $('CHECK FOR NEW MAIL').item.json.uid }}",
|
|
"destinationMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Completed",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Completed"
|
|
}
|
|
},
|
|
"id": "93f53e58-b89d-49a1-8826-d718103dfccd",
|
|
"name": "MoveEmail email2 (Multi)",
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
240,
|
|
1360
|
|
],
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "127fa8c0-f3e8-4267-84da-99d5cf816713",
|
|
"name": "contact_name",
|
|
"value": "={{ $('Limit').item.json.envelope?.sender?.[0]?.name || 'Unknown - needs review' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "1e4e6ac1-80dc-4960-bc7d-f42195b3b46a",
|
|
"name": "email_sender",
|
|
"value": "={{ $('Limit').item.json.envelope?.sender?.[0]?.address }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "4446dc18-a58a-47be-aff3-348aef3f0a5b",
|
|
"name": "customer",
|
|
"value": "xmd68ps32k1x5fu",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "2cd882aa-d188-4d17-9c73-bf2d46bfe45b",
|
|
"name": "email_body",
|
|
"value": "={{ ($('Limit').item.json.textContent || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "047b10fe-9229-48c0-95c2-354405c74025",
|
|
"name": "email_message_id",
|
|
"value": "={{ $('Limit').item.json.headers?.['message-id'] }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "9b2bb6ca-91e8-45dd-9f48-d476080b383e",
|
|
"name": "email_subject",
|
|
"value": "={{ $('Limit').item.json.envelope?.subject || $('Limit').item.json.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "386cbdfa-e906-44e4-ac13-8530dae92a71",
|
|
"name": "order_number",
|
|
"value": "NO-REF",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "c7f6408c-aec8-4055-bf70-66bbc13cecbd",
|
|
"name": "company",
|
|
"value": "fufdg0k9q128wzv",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "13892b22-b4ca-4447-bb25-a945aef8e18c",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "1d6eb541-bd2f-4943-85de-2374c15212e7",
|
|
"name": "description",
|
|
"value": "=[NEEDS REVIEW] AUTO-FLAGGED: AI Agent could not extract any line items from this order email. Raw extracted text: {{ ($('Code in JavaScript4').item.json.aggregatedText || '').substring(0, 2000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "e415897a-8942-4737-8bec-f48e19a106d0",
|
|
"name": "rep",
|
|
"value": "={{ $('Limit').item.json.rep }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "97cf61bd-97ee-4c3b-a41b-ffc4d50f996b",
|
|
"name": "Create order (single - no items - raw)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2800,
|
|
2624
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"formBinaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] ?? '' }} ",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"id": "bfd49b45-1d0e-4750-a775-bb3dbf24a0a8",
|
|
"name": "Update order (attach raw file - single)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3840,
|
|
2480
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create orders').first().json.id;\n\n// Use only what was actually passed to this node (not the full Split Attachments1 output)\n// so downstream SplitInBatches loops don't re-inject filtered-out items.\nconst attachmentItems = $input.all().filter(i => {\n if (!i.binary || Object.keys(i.binary).length === 0) return false;\n const bKeys = Object.keys(i.binary);\n return bKeys.some(k => i.binary[k].fileName && i.binary[k].fileName !== 'None' && i.binary[k].fileName !== 'null');\n});\n\nif (attachmentItems.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn attachmentItems.map(i => {\n const cleanBinary = {};\n for (const [k, v] of Object.entries(i.binary)) {\n if (!v.fileName || v.fileName === 'None' || v.fileName === 'null') continue;\n cleanBinary[k] = v;\n }\n if (Object.keys(cleanBinary).length === 0) return null;\n return {\n json: { orderId, noAttachments: false },\n binary: cleanBinary,\n };\n}).filter(Boolean);"
|
|
},
|
|
"id": "0102cef1-35cc-4496-b844-56c13b3088fa",
|
|
"name": "Prepare attachment list",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3248,
|
|
1648
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "b4ce7c83-a9f2-4d5d-b6f9-92d572b6c2f1",
|
|
"name": "Attachment loop",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
3472,
|
|
1648
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"combinator": "and",
|
|
"conditions": [
|
|
{
|
|
"id": "d7345329-0208-4a34-ae62-eaab8f0ea5c8",
|
|
"leftValue": "={{ $json.noAttachments }}",
|
|
"operator": {
|
|
"operation": "notEquals",
|
|
"type": "boolean"
|
|
},
|
|
"rightValue": true
|
|
},
|
|
{
|
|
"id": "a9c12345-file-name-valid",
|
|
"leftValue": "={{ Object.values($binary || {}).length > 0 && Object.values($binary)[0].fileName != null && Object.values($binary)[0].fileName !== 'None' && Object.values($binary)[0].fileName !== 'null' }}",
|
|
"operator": {
|
|
"operation": "equals",
|
|
"type": "boolean"
|
|
},
|
|
"rightValue": true
|
|
}
|
|
],
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
}
|
|
}
|
|
},
|
|
"id": "88a759c4-8c13-43c7-b868-d243c166b836",
|
|
"name": "Has attachment to upload?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
3696,
|
|
1232
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create orders (Multi)').first().json.id;\nconst attachmentItems = $('Split Attachments1 (Multi)').all().filter(i => i.binary && Object.keys(i.binary).length > 0);\n\nif (attachmentItems.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn attachmentItems.map(i => ({\n json: { orderId, noAttachments: false },\n binary: i.binary,\n}));"
|
|
},
|
|
"id": "434ba73b-067c-4e1d-a564-e77779a16aca",
|
|
"name": "Prepare attachment list (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3024,
|
|
960
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "211984cb-52b7-47c5-aa76-09c6bdc12aa7",
|
|
"name": "Attachment loop (Multi)",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
3248,
|
|
960
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "7ba3601c-53d0-4117-a891-b54d639dc346",
|
|
"leftValue": "={{ $json.noAttachments }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "notEquals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "fafe7217-f34e-4cae-a87a-9eea01fcab81",
|
|
"name": "Has attachment to upload? (Multi)",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
3472,
|
|
896
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create order (no PO detected - raw)').first().json.id;\nconst binary = $input.first().binary || {};\nconst keys = Object.keys(binary);\n\nif (keys.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn keys.map(k => ({\n json: { orderId, noAttachments: false },\n binary: { [k]: binary[k] },\n}));"
|
|
},
|
|
"id": "ab4444a7-5f75-4177-a342-740a2478397b",
|
|
"name": "Prepare attachment list (whole email)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-208,
|
|
1984
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "bf6b5c3f-a8b9-49c7-aaa8-56d2d90f31e9",
|
|
"name": "Attachment loop (whole email)",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
16,
|
|
1984
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create order (no items extracted - raw)').first().json.id;\nconst attachmentItems = $('Split Attachments1 (Multi)').all().filter(i => i.binary && Object.keys(i.binary).length > 0);\n\nif (attachmentItems.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn attachmentItems.map(i => ({\n json: { orderId, noAttachments: false },\n binary: i.binary,\n}));"
|
|
},
|
|
"id": "b909e644-97be-4bb7-ae8a-a7e4bb1c1c83",
|
|
"name": "Prepare attachment list (per order)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2800,
|
|
1152
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "58907510-2052-408f-85fe-2811aff80f32",
|
|
"name": "Attachment loop (per order)",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
3024,
|
|
1152
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create order (single - no items - raw)').first().json.id;\n\nconst attachmentItems = $input.all().filter(i => {\n if (!i.binary || Object.keys(i.binary).length === 0) return false;\n const bKeys = Object.keys(i.binary);\n return bKeys.some(k => i.binary[k].fileName && i.binary[k].fileName !== 'None' && i.binary[k].fileName !== 'null');\n});\n\nif (attachmentItems.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn attachmentItems.map(i => {\n const cleanBinary = {};\n for (const [k, v] of Object.entries(i.binary)) {\n if (!v.fileName || v.fileName === 'None' || v.fileName === 'null') continue;\n cleanBinary[k] = v;\n }\n if (Object.keys(cleanBinary).length === 0) return null;\n return {\n json: { orderId, noAttachments: false },\n binary: cleanBinary,\n };\n}).filter(Boolean);"
|
|
},
|
|
"id": "1b210b9b-465b-4836-bd84-a05a479adf7f",
|
|
"name": "Prepare attachment list (single fallback)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3024,
|
|
2624
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "fbd21c7d-4c30-4b36-b53d-a0bff29eb082",
|
|
"name": "Attachment loop (single fallback)",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
3248,
|
|
2576
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "MiniMax-M2.5",
|
|
"options": {}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1728,
|
|
1184
|
|
],
|
|
"id": "d3cb0978-d756-41e3-8d25-5b1f5e215b26",
|
|
"name": "MiniMax Chat Model2",
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "a1b2c3d4-has-binary",
|
|
"leftValue": "={{ $json.noAttachments }}",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
},
|
|
"rightValue": "false"
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {}
|
|
},
|
|
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
"name": "Has binary? (single fallback)",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
3504,
|
|
2496
|
|
]
|
|
},
|
|
{
|
|
"id": "f5d15e31-5ee7-49d4-a4e3-2a21e15bf2bf",
|
|
"name": "Sanitise binary for upload",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3808,
|
|
1328
|
|
],
|
|
"parameters": {
|
|
"jsCode": "// Last gate before PocketBase upload:\n// Strip any binary entry that has no fileName (inline email images)\n// If nothing valid remains, skip this item entirely.\nconst item = $input.item;\nconst cleanBinary = {};\n\nfor (const [k, v] of Object.entries(item.binary || {})) {\n const name = v.fileName;\n if (name && name !== 'None' && name !== 'null' && name.trim() !== '') {\n cleanBinary[k] = v;\n }\n}\n\n// No valid files on this item \u2014 drop it silently\nif (Object.keys(cleanBinary).length === 0) {\n return [];\n}\n\nreturn [{\n json: item.json,\n binary: cleanBinary,\n}];"
|
|
}
|
|
},
|
|
{
|
|
"id": "282149bd-070a-41dd-9c4b-468f1d0b6aa1",
|
|
"name": "SetEmailFlags email (mark on fetch)",
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1700,
|
|
1456
|
|
],
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "setEmailFlags",
|
|
"mailboxPath": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $('CHECK FOR NEW MAIL').item.json.uid }}",
|
|
"flags": {
|
|
"\\Seen": true
|
|
}
|
|
},
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "getList",
|
|
"filter": "={{ (() => { const msgId = $('Limit').item.json.headers['message-id'] || ''; if (!msgId) return 'id = \"__no_match__\"'; return 'email_message_id = \"' + msgId.replace(/\"/g, '\\\\\"') + '\"'; })() }}",
|
|
"options": {
|
|
"elementsPerPage": 1
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "0c8d53f6-057e-4dc2-b0ad-7add37ae26d3",
|
|
"name": "Search orders by email_message_id",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2688,
|
|
1952
|
|
],
|
|
"alwaysOutputData": true,
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "213840b0-2603-42cb-bb16-a9daec2967ca",
|
|
"leftValue": "={{ $input.all().length }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {}
|
|
},
|
|
"id": "d74269ce-9c44-4e6d-8a07-294b7045487c",
|
|
"name": "Is new order?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
2880,
|
|
1952
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Branch hit when 'Search orders by email_message_id' returns \u22651 record.\n// The order already exists (likely a duplicate poll). Skip the entire\n// create chain and move the email to Completed so the next poll\n// doesn't pick it up. Emit a breadcrumb so the operator can see in\n// the execution log that this was a dedup-skip.\n//\n// See docs/audit/order-pipeline-investigation-2026-08-20.md \u00a72.\nconst limit = $('Limit').item.json;\nconst skipped = $('Search orders by email_message_id').all().map(o => ({\n id: o.json.id,\n order_number: o.json.order_number,\n created: o.json.created,\n}));\nreturn [{\n json: {\n dedup: 'skipped_existing_order',\n message_id: limit.headers?.['message-id'] || '',\n subject: limit.envelope?.subject || '',\n from: limit.envelope?.sender?.[0]?.address || '',\n skipped_orders: skipped,\n },\n}];"
|
|
},
|
|
"id": "d47aa8af-223d-4981-9062-51426dceef69",
|
|
"name": "Skip reprocess: log breadcrumb",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3072,
|
|
2176
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "getList",
|
|
"filter": "={{ (() => { const msgId = $('Limit (Multi)').item.json.headers['message-id'] || ''; if (!msgId) return 'id = \"__no_match__\"'; return 'email_message_id = \"' + msgId.replace(/\"/g, '\\\\\"') + '\"'; })() }}",
|
|
"options": {
|
|
"elementsPerPage": 1
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "e9507973-bc42-4321-83e5-db03870ab9bd",
|
|
"name": "Search orders by email_message_id (Multi)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2464,
|
|
688
|
|
],
|
|
"alwaysOutputData": true,
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "9beaba87-2ad2-44e8-84f3-493018e4cc30",
|
|
"leftValue": "={{ $input.all().length }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {}
|
|
},
|
|
"id": "814488bc-92e9-4ef3-bb83-2fa2e9c0286d",
|
|
"name": "Is new order? (Multi)",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
2656,
|
|
688
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Multi-branch dedup skip. Same contract as the single-branch\n// 'Skip reprocess: log breadcrumb' (see that node's comment).\nconst limit = $('Limit (Multi)').item.json;\nconst skipped = $('Search orders by email_message_id (Multi)').all().map(o => ({\n id: o.json.id,\n order_number: o.json.order_number,\n created: o.json.created,\n}));\nreturn [{\n json: {\n dedup: 'skipped_existing_order',\n message_id: limit.headers?.['message-id'] || '',\n subject: limit.envelope?.subject || '',\n from: limit.envelope?.sender?.[0]?.address || '',\n skipped_orders: skipped,\n },\n}];"
|
|
},
|
|
"id": "75187ae9-3aa6-4044-91fd-21fa19bdb687",
|
|
"name": "Skip reprocess: log breadcrumb (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2848,
|
|
880
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Deterministic contact_name extraction.\n//\n// Runs BEFORE the AI Agent. Extracts a name from the email body\n// (signature block) and the attached PDF text (Requester / Deliver To\n// / Attention / End-User lines). Returns { contact_name } so the AI\n// Agent's prompt can use it as a hint.\n//\n// Why \"deterministic first, LLM second\"? The LLM is non-deterministic\n// even at temperature 0.1 \u2014 the same email has produced 13 different\n// contact_name values across its 138 duplicate orders. The PDF\n// \"Requester:\" line is a clean, fixed field. The email signature\n// (last block after \"Regards\") is a clean, fixed field. Extracting\n// them with regex is more reliable than asking the LLM to find them.\n//\n// See docs/audit/order-pipeline-investigation-2026-08-20.md \u00a73.\n//\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 1. Email signature parse\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst emailItem = $('Limit').item.json;\nconst body = emailItem.textContent || emailItem.text || '';\n\nlet emailCandidate = '';\nconst signatureMarkers = [\n /\\bregards\\b/i,\n /\\bkind regards\\b/i,\n /\\bbest regards\\b/i,\n /\\bbest\\b/i,\n /\\bthanks\\b/i,\n /\\bthank you\\b/i,\n /\\bcheers\\b/i,\n /\\bwith thanks\\b/i,\n];\nlet signatureStart = -1;\nfor (const re of signatureMarkers) {\n const m = body.match(re);\n if (m && (signatureStart === -1 || m.index < signatureStart)) {\n signatureStart = m.index + m[0].length;\n }\n}\nif (signatureStart > 0) {\n const sigBlock = body.slice(signatureStart).trim();\n const lines = sigBlock.split(/\\r?\\n/).map(l => l.trim()).filter(Boolean);\n for (const line of lines) {\n if (line.length > 60) continue;\n if (line.includes('@')) continue;\n if (/https?:\\/\\//i.test(line)) continue;\n if (!/[a-zA-Z]/.test(line)) continue;\n const cleaned = line.replace(/^(mr|mrs|ms|dr|prof)\\.?\\s+/i, '').trim();\n if (cleaned.length > 1) {\n emailCandidate = cleaned;\n break;\n }\n }\n}\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 2. PDF \"Requester\" / \"Deliver To\" / \"Attention\" parse\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet pdfCandidate = '';\ntry {\n const aggregated = $('Code in JavaScript4').item.json.aggregatedText || '';\n const patterns = [\n /\\b(?:requester|requestor|deliver\\s*to|attention|attention\\s*to|end[-\\s]?user|bill\\s*to|contact)\\s*[:\\-]\\s*([^\\n\\r]{3,80})/i,\n ];\n for (const re of patterns) {\n const m = aggregated.match(re);\n if (m && m[1]) {\n let v = m[1].trim();\n v = v.replace(/^(mr|mrs|ms|dr|prof)\\.?\\s+/i, '');\n v = v.split(/\\s{2,}|\\s+(?:email|phone|tel|contact|buyer|reference|po)\\s*[:\\-]/i)[0].trim();\n if (v.length >= 3 && v.length <= 80 && /[a-zA-Z]/.test(v)) {\n pdfCandidate = v;\n break;\n }\n }\n }\n} catch (_e) {\n // Code in JavaScript4 may not have run yet \u2014 fall back to email sig.\n}\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// 3. Pick the best candidate (PDF > email signature > nothing)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst final = pdfCandidate || emailCandidate || '';\nreturn [{\n json: {\n contact_name: final,\n pdf_candidate: pdfCandidate,\n email_candidate: emailCandidate,\n source: pdfCandidate ? 'pdf_requester' : (emailCandidate ? 'email_signature' : 'none'),\n },\n}];\n"
|
|
},
|
|
"id": "820c8fc4-4853-45b7-a0ae-1d934e21eddf",
|
|
"name": "Extract contact name (deterministic)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2240,
|
|
1600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Deterministic contact_name extraction (multi-branch).\n// Same algorithm as the single-branch 'Extract contact name\n// (deterministic)' \u2014 reads $('Limit (Multi)').item.json for the\n// email body and $('Code in JavaScript4 (Multi)').item.json for\n// the PDF text. Returns { contact_name } for the AI Agent.\n//\n// See docs/audit/order-pipeline-investigation-2026-08-20.md \u00a73.\nconst emailItem = $('Limit (Multi)').item.json;\nconst body = emailItem.textContent || emailItem.text || '';\n\nlet emailCandidate = '';\nconst signatureMarkers = [\n /\\bregards\\b/i,\n /\\bkind regards\\b/i,\n /\\bbest regards\\b/i,\n /\\bbest\\b/i,\n /\\bthanks\\b/i,\n /\\bthank you\\b/i,\n /\\bcheers\\b/i,\n /\\bwith thanks\\b/i,\n];\nlet signatureStart = -1;\nfor (const re of signatureMarkers) {\n const m = body.match(re);\n if (m && (signatureStart === -1 || m.index < signatureStart)) {\n signatureStart = m.index + m[0].length;\n }\n}\nif (signatureStart > 0) {\n const sigBlock = body.slice(signatureStart).trim();\n const lines = sigBlock.split(/\\r?\\n/).map(l => l.trim()).filter(Boolean);\n for (const line of lines) {\n if (line.length > 60) continue;\n if (line.includes('@')) continue;\n if (/https?:\\/\\//i.test(line)) continue;\n if (!/[a-zA-Z]/.test(line)) continue;\n const cleaned = line.replace(/^(mr|mrs|ms|dr|prof)\\.?\\s+/i, '').trim();\n if (cleaned.length > 1) {\n emailCandidate = cleaned;\n break;\n }\n }\n}\n\nlet pdfCandidate = '';\ntry {\n const aggregated = $('Code in JavaScript4 (Multi)').item.json.aggregatedText || '';\n const patterns = [\n /\\b(?:requester|requestor|deliver\\s*to|attention|attention\\s*to|end[-\\s]?user|bill\\s*to|contact)\\s*[:\\-]\\s*([^\\n\\r]{3,80})/i,\n ];\n for (const re of patterns) {\n const m = aggregated.match(re);\n if (m && m[1]) {\n let v = m[1].trim();\n v = v.replace(/^(mr|mrs|ms|dr|prof)\\.?\\s+/i, '');\n v = v.split(/\\s{2,}|\\s+(?:email|phone|tel|contact|buyer|reference|po)\\s*[:\\-]/i)[0].trim();\n if (v.length >= 3 && v.length <= 80 && /[a-zA-Z]/.test(v)) {\n pdfCandidate = v;\n break;\n }\n }\n }\n} catch (_e) {\n // Code in JavaScript4 (Multi) may not have run yet \u2014 fall back to email sig.\n}\n\nconst final = pdfCandidate || emailCandidate || '';\nreturn [{\n json: {\n contact_name: final,\n pdf_candidate: pdfCandidate,\n email_candidate: emailCandidate,\n source: pdfCandidate ? 'pdf_requester' : (emailCandidate ? 'email_signature' : 'none'),\n },\n}];\n"
|
|
},
|
|
"id": "1f3c39cb-5b99-424a-a26f-c1f0f7ce27f7",
|
|
"name": "Extract contact name (deterministic) (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1888,
|
|
480
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Override contact_name with the deterministic extractor result.\n// Belt-and-braces: the AI Agent's system prompt already says to use\n// the deterministic name when present, but LLMs are LLMs. This node\n// runs AFTER the AI Agent and forces the result: if the deterministic\n// extractor found a name, replace whatever the LLM produced.\n// Falls back to the LLM's output only when the deterministic\n// extractor found nothing.\n//\n// See docs/audit/order-pipeline-investigation-2026-08-20.md \u00a73.\nconst ai = $('AI Agent').item.json;\nconst det = $('Extract contact name (deterministic)').item.json;\nconst detName = (det.contact_name || '').trim();\n\nconst output = ai.output || {};\nconst orderInfo = output.order_info || {};\nconst originalLLM = (orderInfo.contact_name || '').trim();\n\nif (detName) {\n // Deterministic extractor found a name; use it.\n orderInfo.contact_name = detName;\n} else if (!originalLLM) {\n // Neither deterministic nor LLM produced a name \u2014 that's fine,\n // it stays empty. The page renderer will skip it.\n orderInfo.contact_name = '';\n}\n// else: LLM produced a name and deterministic didn't \u2014 keep the LLM's.\n\noutput.order_info = orderInfo;\nreturn [{ json: { output, __contact_name_source: detName ? 'deterministic' : (originalLLM ? 'llm' : 'none') } }];\n"
|
|
},
|
|
"id": "b9f32fba-43d9-4eab-b330-923a77bf7efe",
|
|
"name": "Override contact_name with deterministic",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3500,
|
|
2624
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Override contact_name with deterministic extractor (multi-branch).\n// Same as the single-branch 'Override contact_name with deterministic'.\nconst ai = $('AI Agent (Multi)').item.json;\nconst det = $('Extract contact name (deterministic) (Multi)').item.json;\nconst detName = (det.contact_name || '').trim();\n\nconst output = ai.output || {};\nconst orderInfo = output.order_info || {};\nconst originalLLM = (orderInfo.contact_name || '').trim();\n\nif (detName) {\n orderInfo.contact_name = detName;\n} else if (!originalLLM) {\n orderInfo.contact_name = '';\n}\noutput.order_info = orderInfo;\nreturn [{ json: { output, __contact_name_source: detName ? 'deterministic' : (originalLLM ? 'llm' : 'none') } }];\n"
|
|
},
|
|
"id": "7ed380a1-7e53-4e8a-897a-a6901fcea468",
|
|
"name": "Override contact_name with deterministic (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3120,
|
|
1440
|
|
]
|
|
}
|
|
],
|
|
"connections": {
|
|
"Analyze image1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript3",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Switch1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Extract PDF",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from CSV1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from XLS1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from XLSX1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from TXT1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 2
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Analyze image1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Analyze image1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract PDF",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript3": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 6
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from CSV1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 2
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from TXT1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 5
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from XLSX1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 4
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from XLS1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 3
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create order_items1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Merge1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "PocketBase1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Search customers": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create orders",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Minimax Chat Model4": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "Structured Output Parser",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Structured Output Parser": {
|
|
"ai_outputParser": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "ai_outputParser",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"AI Agent": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Override contact_name with deterministic",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Minimax Chat Model": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract PDF": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If3",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Split Attachments1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Switch1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Merge2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"PocketBase1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Limit": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Download ATTACHMENT",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Search orders by email_message_id",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Create order (single - no items - raw)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Workflow",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "MoveEmail email2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "MoveEmail email2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"MESSAGE FOR AI": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Aggregate": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MESSAGE FOR AI",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"MiniMax Chat Model": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Structured Output Parser1": {
|
|
"ai_outputParser": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_outputParser",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"AI Agent1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Ai Resposes",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"MiniMax Chat Model1": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "Structured Output Parser1",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Ai Resposes": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Switch",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Switch": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "UPDATE AI ORDER",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "UPDATE AI ORDER ITEM",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_suppliers": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_price_list": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_order_items": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_stock": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_orders": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_quote_items": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_quotes": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Ollama Chat Model": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_languageModel",
|
|
"index": 1
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"CHECK FOR NEW MAIL": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "SetEmailFlags email (mark on fetch)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Schedule Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "CHECK FOR NEW MAIL",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order_items1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Aggregate",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"UPDATE AI ORDER": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"UPDATE AI ORDER ITEM": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create orders": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript2",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Merge1",
|
|
"type": "main",
|
|
"index": 1
|
|
},
|
|
{
|
|
"node": "Prepare attachment list",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Download ATTACHMENT": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Split Attachments1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"PDF to PNG": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Analyze image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If3": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 1
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "PDF to PNG",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Analyze image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update orders": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript4": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Extract contact name (deterministic)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
],
|
|
"ai_languageModel": [],
|
|
"ai_outputParser": [],
|
|
"ai_tool": [],
|
|
"ai_memory": [],
|
|
"ai_embedding": [],
|
|
"ai_document": []
|
|
},
|
|
"MoveEmail email2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "SetEmailFlags email",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Send an Email": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Classify Email (Deterministic)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Email",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Email": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send an Email",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Download ATTACHMENT (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "AI Classify Email (cheap)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Limit",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Cheap Classifier Model (Minimax)": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Classify Email (cheap)",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"AI Classify Email (cheap)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse AI Classification",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse AI Classification": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Email (AI Result)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Email (AI Result)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send an Email",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Limit",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Download ATTACHMENT (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Group attachments by PO",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Group attachments by PO": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Has valid orders?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Has valid orders?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Per-order loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Create order (no PO detected - raw)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Per-order loop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email2 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Limit (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Limit (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Split Attachments1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Split Attachments1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Switch1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Switch1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Extract PDF (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from CSV1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from XLS1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from XLSX1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from TXT1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Analyze image1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Analyze image1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract PDF (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract PDF (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If3 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If3 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "PDF to PNG (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"PDF to PNG (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Analyze image (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Analyze image (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from CSV1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from XLS1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from XLSX1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from TXT1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Analyze image1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript3 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript3 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript4 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Extract contact name (deterministic) (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
],
|
|
"ai_languageModel": [],
|
|
"ai_outputParser": [],
|
|
"ai_tool": [],
|
|
"ai_memory": [],
|
|
"ai_embedding": [],
|
|
"ai_document": []
|
|
},
|
|
"Minimax Chat Model (Multi)": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent (Multi)",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Structured Output Parser (Multi)": {
|
|
"ai_outputParser": [
|
|
[
|
|
{
|
|
"node": "AI Agent (Multi)",
|
|
"type": "ai_outputParser",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"AI Agent (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Override contact_name with deterministic (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Search orders by email_message_id (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Create order (no items extracted - raw)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Search customers (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create orders (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create orders (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript2 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Prepare attachment list (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript2 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create order_items1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order_items1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge order completion",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update orders (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Merge order completion": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Per-order loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order (no PO detected - raw)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare attachment list (whole email)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update order (attach raw file - whole email)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (whole email)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order (no items extracted - raw)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare attachment list (per order)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update order (attach raw file - per order)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (per order)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order (single - no items - raw)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare attachment list (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update order (attach raw file - single)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Has attachment to upload?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Has attachment to upload?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Sanitise binary for upload",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Attachment loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge order completion",
|
|
"type": "main",
|
|
"index": 1
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Has attachment to upload? (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Has attachment to upload? (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Update orders (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Attachment loop (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list (whole email)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (whole email)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop (whole email)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email2 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Update order (attach raw file - whole email)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list (per order)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (per order)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop (per order)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Per-order loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Update order (attach raw file - per order)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list (single fallback)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop (single fallback)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Has binary? (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"MiniMax Chat Model2": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "Structured Output Parser (Multi)",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Has binary? (single fallback)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Update order (attach raw file - single)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Attachment loop (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Sanitise binary for upload": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Update orders",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"SetEmailFlags email (mark on fetch)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Classify Email (Deterministic)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Search orders by email_message_id": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Is new order?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Is new order?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Search customers",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Skip reprocess: log breadcrumb",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Skip reprocess: log breadcrumb": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Search orders by email_message_id (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Is new order? (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Is new order? (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Search customers (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Skip reprocess: log breadcrumb (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Skip reprocess: log breadcrumb (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email2 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract contact name (deterministic)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract contact name (deterministic) (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "AI Agent (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Override contact_name with deterministic": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Override contact_name with deterministic (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"binaryMode": "separate",
|
|
"availableInMCP": true,
|
|
"errorWorkflow": "kVylcetxy858PcfP"
|
|
},
|
|
"staticData": {
|
|
"node:Schedule Trigger": {
|
|
"recurrenceRules": [
|
|
null
|
|
],
|
|
"recurrenceRuleSignatures": [
|
|
null
|
|
]
|
|
}
|
|
},
|
|
"meta": {
|
|
"templateCredsSetupCompleted": true,
|
|
"aiBuilderAssisted": true,
|
|
"builderVariant": "mcp"
|
|
},
|
|
"nodeGroups": [],
|
|
"pinData": {
|
|
"Schedule Trigger": [
|
|
{
|
|
"json": {
|
|
"timestamp": "2026-08-18T14:10:43.006+00:00",
|
|
"Readable date": "August 18th 2026, 2:10:43 pm",
|
|
"Readable time": "2:10:43 pm",
|
|
"Day of week": "Tuesday",
|
|
"Year": "2026",
|
|
"Month": "August",
|
|
"Day of month": "18",
|
|
"Hour": "14",
|
|
"Minute": "10",
|
|
"Second": "43",
|
|
"Timezone": "UTC (UTC+00:00)"
|
|
},
|
|
"pairedItem": {
|
|
"item": 0
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"versionId": "c7ffa24e-cc0f-4863-b6ae-cdaf6a8c4b41",
|
|
"activeVersionId": "c7ffa24e-cc0f-4863-b6ae-cdaf6a8c4b41",
|
|
"versionCounter": 59,
|
|
"triggerCount": 1,
|
|
"sourceWorkflowId": null,
|
|
"shared": [
|
|
{
|
|
"updatedAt": "2026-08-15T22:15:05.317Z",
|
|
"createdAt": "2026-08-15T22:15:05.317Z",
|
|
"role": "workflow:owner",
|
|
"workflowId": "DWkxko0SS1uoelsg",
|
|
"projectId": "UhHq8eAZFxiYmQxH",
|
|
"project": {
|
|
"updatedAt": "2026-06-11T15:00:27.533Z",
|
|
"createdAt": "2026-06-11T14:30:25.543Z",
|
|
"id": "UhHq8eAZFxiYmQxH",
|
|
"name": "David Els <david.els1@outlook.com>",
|
|
"type": "personal",
|
|
"icon": null,
|
|
"description": null,
|
|
"customTelemetryTags": [],
|
|
"creatorId": "4f770df2-bd2f-4436-b153-b27004efc7b6"
|
|
}
|
|
}
|
|
],
|
|
"tags": [],
|
|
"activeVersion": {
|
|
"updatedAt": "2026-08-18T21:57:21.334Z",
|
|
"createdAt": "2026-08-18T21:57:21.334Z",
|
|
"versionId": "c7ffa24e-cc0f-4863-b6ae-cdaf6a8c4b41",
|
|
"workflowId": "DWkxko0SS1uoelsg",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Normalize Image Text\"\nconst item = $input.first();\nconst extractedText = item.json.content?.[0]?.text || '';\nreturn [{\n json: {\n ...item.json,\n text: extractedText\n }\n}];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1136,
|
|
3040
|
|
],
|
|
"id": "4bdaa1df-2d49-4efb-a911-79c180707823",
|
|
"name": "Code in JavaScript3"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "image",
|
|
"modelId": {
|
|
"__rl": true,
|
|
"value": "MiniMax-M3",
|
|
"mode": "list",
|
|
"cachedResultName": "MiniMax-M3"
|
|
},
|
|
"inputType": "binary",
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"options": {}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.anthropic",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
912,
|
|
3040
|
|
],
|
|
"id": "65a1b6ea-7eda-475a-8750-0862bafcb308",
|
|
"name": "Analyze image1",
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "jLsa9I3U2W6vqzeE",
|
|
"name": "Anthropic account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
2144
|
|
],
|
|
"id": "c7ae6cf1-0d69-4405-a9f0-99d84c6a7d3f",
|
|
"name": "Extract from CSV1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "text",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
2848
|
|
],
|
|
"id": "4766ba0f-b5e5-434d-af89-c0acf6e0eb56",
|
|
"name": "Extract from TXT1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xlsx",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
2656
|
|
],
|
|
"id": "4a6f899a-3636-4068-8c55-da6d74c01ec3",
|
|
"name": "Extract from XLSX1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xls",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
2336
|
|
],
|
|
"id": "8df30e7b-72f8-4f97-89ea-50b5a117f8e3",
|
|
"name": "Extract from XLS1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "application/pdf",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
},
|
|
"id": "72a24273-44ef-4691-9a59-feabeb2663f3"
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "34b35229-1280-49bd-8a0a-d17025841a70",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/csv",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "71fff6af-219b-4b35-85e9-3f3d6ce852f6",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/xls",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "8e69249c-b0ef-4a08-9af5-a7afc25c02e4",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/xlsx",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "7c11e0a4-6884-4c5b-ad46-ebc76b9b68cb",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "=text/text",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "41403feb-e786-488c-b1d4-eecd007e3cd0",
|
|
"leftValue": "={{ $('Limit').item.json.text || $('Limit').item.json.textContent }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "751252c4-116c-42f3-9709-ddbd4a372423",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "image/png",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "11532f4b-1b10-4596-b255-b824aec56c1f",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "image/jpeg",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "124a0978-e621-43a2-b244-54be8da0e30b",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "application/octet-stream",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {
|
|
"allMatchingOutputs": true
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
240,
|
|
2304
|
|
],
|
|
"id": "57c97b41-8fa5-4f77-a849-d4320a6c4702",
|
|
"name": "Switch1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Split Order Items\"\nconst orderId = $('Create orders').item.json.id;\nconst items = $('AI Agent').item.json.output.order_items;\n\nreturn items.map(item => ({\n json: {\n order: orderId,\n description: item.description,\n quantity: item.quantity,\n selling_price: item.unit_price,\n notes: item.notes\n }\n}));\n"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3248,
|
|
2032
|
|
],
|
|
"id": "0a6ad492-45d7-4849-8828-f16abb29ff4d",
|
|
"name": "Code in JavaScript2"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "order_items",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "order-1",
|
|
"name": "order",
|
|
"value": "={{ $json.order }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "description-1",
|
|
"name": "description",
|
|
"value": "={{ $json.description }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "quantity-1",
|
|
"name": "quantity",
|
|
"value": "={{ $json.quantity }}",
|
|
"type": "number"
|
|
},
|
|
{
|
|
"id": "notes-1",
|
|
"name": "notes",
|
|
"value": "={{ $json.notes }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "d29a84cf-bb1e-4cfd-8665-3c4164aa3103",
|
|
"name": "item_status",
|
|
"value": "pending",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "0b83a5b4-d1f5-43fc-a712-909ddabe3bb0",
|
|
"name": "selling_price",
|
|
"value": "={{ $json.selling_price }}",
|
|
"type": "number"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3472,
|
|
2032
|
|
],
|
|
"id": "3652cf1b-e203-4076-b5c5-6b6b2b48bbaf",
|
|
"name": "Create order_items1",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"mode": "combine",
|
|
"combineBy": "combineAll",
|
|
"options": {
|
|
"clashHandling": {
|
|
"values": {
|
|
"resolveClash": "addSuffix"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.merge",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
3696,
|
|
1424
|
|
],
|
|
"id": "a75be0b6-50c4-4b0f-af24-f466d871ee25",
|
|
"name": "Merge1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"collection": "=pbc_3527180448",
|
|
"action": "update",
|
|
"recordId": "={{ $json.orderId_1 }}",
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "=order",
|
|
"value": "={{ $json.id_2 }}"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"type": "n8n-nodes-pocketbase-admin.pocketBaseAdmin",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3920,
|
|
1616
|
|
],
|
|
"id": "75113d56-619f-4f5b-b912-8ba85549ab71",
|
|
"name": "PocketBase1",
|
|
"credentials": {
|
|
"pocketBaseAdminApi": {
|
|
"id": "UjiyPhf4vcCV6eE2",
|
|
"name": "PocketBase Admin account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "customers",
|
|
"parameters": {
|
|
"elementsPerPage": 12,
|
|
"filter": "={{ \n (() => {\n const name = $('AI Agent').item.json.output.order_info.contact_name || '';\n const email = $('AI Agent').item.json.output.order_info.order_info?.sender_email || $('AI Agent').item.json.output.order_info.sender_email || '';\n \n // 1. Clean and split the name keywords\n const nameKeywords = name.replace(/mr\\.|mrs\\.|ms\\.|mr|mrs|ms/gi, '').split(/[\\s,]+/).filter(w => w.length > 2);\n const nameQuery = nameKeywords.length > 0 ? nameKeywords.map(w => `customer ~ '${w}'`).join(' && ') : '';\n \n // 2. Build the email query if it exists\n const emailQuery = email ? `email ~ '${email}'` : '';\n \n // 3. Force a hard fallback ID only if both name and email are missing\n if (!nameQuery && !emailQuery) {\n return \"id = 'xmd68ps32k1x5fu'\";\n }\n \n // 4. Join name query and email query with || (OR)\n return [nameQuery, emailQuery].filter(Boolean).map(q => `(${q})`).join(' || ');\n })()\n}}"
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "b2da5fe5-9874-4510-86df-bf9fbd33f31c",
|
|
"name": "Search customers",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2800,
|
|
1952
|
|
],
|
|
"alwaysOutputData": true,
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "MiniMax-M2.5",
|
|
"options": {
|
|
"hideThinking": true,
|
|
"temperature": 0.1,
|
|
"timeout": 60000,
|
|
"maxRetries": 3
|
|
}
|
|
},
|
|
"id": "a4bf0ff2-1686-489e-bc15-2f5e205128f0",
|
|
"name": "Minimax Chat Model4",
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2304,
|
|
3072
|
|
],
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsonSchemaExample": "{\n \"order_info\": {\n \"company_name\": \"Harmony\",\n \"contact_name\": \"Mark Wilkie\",\n \"sender_email\": \"davidels1986@gmail.com\",\n \"rfq_number\": \"PO-20260604-004\",\n \"notes\": \"Please quote on Isherwood letterhead\"\n },\n \"order_items\": [\n {\n \"description\": \"TURBO END+ SUPER TOUGH COUNTERSINK 14-60MM\",\n \"quantity\": 2,\n \"unit_price\": 0.00,\n \"notes\": \"SKU: JEIETCSK-60\"\n }\n ]\n}",
|
|
"autoFix": true
|
|
},
|
|
"id": "e1f6cc88-900f-45e1-83f8-0bd28611d6ac",
|
|
"name": "Structured Output Parser",
|
|
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
2224,
|
|
2864
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"promptType": "define",
|
|
"text": "=Email Subject: {{ $('Limit').item.json.envelope.subject }}\n\nSender Email: {{ $('Limit').item.json.envelope.sender[0].address }}\n\nRecipient Email: {{ $('Limit').item.json.envelope.to[0].address }}\n\n--- EMAIL BODY ---\n{{ $('Limit').item.json.textContent }}\n\n\n--- ORDER DETAILS ---\n\n\n\n--- ATTACHMENT TEXT (ALL PDFs COMBINED) ---\n{{ $json.aggregatedText }}\n\n\n\n",
|
|
"hasOutputParser": true,
|
|
"options": {
|
|
"systemMessage": "You are an expert procurement and data extraction AI. Your task is to process incoming orders by analyzing both the email body and any attached document text.\n\nYou must reconcile the information. For example, if the email says \"Please quote the attached 5 items,\" you must extract the company details from the email signature and the 5 items from the attachment text.\n\nExtract the data and return it STRICTLY as a valid JSON object matching the exact structure below. Do not include markdown formatting (like ```json), conversational text, or explanations. Only return the JSON.\n\nJSON STRUCTURE:\n{\n \"order_info\": {\n \"company_name\": \"Extract the buyer/end-customer company name.\",\n \"contact_name\": \"Extract the actual End-User or Requester of the goods (Look for fields like 'Requester', 'Deliver To', 'Attention', or names in the Notes). Do NOT use the 'Buyer' or 'Created By' name if a Requester is listed.\",\n \"sender_email\": \"Extract the email address of the person requesting the quote.\",\n \"rfq_number\": \"Extract any visible RFQ, Reference, or Order number provided by the customer. Leave empty if none.\",\n \"notes\": \"Summarize any specific instructions, delivery requirements, or context from the email body (e.g., 'Urgent delivery required').\"\n },\n \"order_items\": [\n {\n \"description\": \"The exact name, SKU, or description of the requested item.\",\n \"quantity\": 1,\n \"unit_price\": 0.00,\n \"notes\": \"Any specific unit of measure (e.g., 'meters', 'kg') or specific notes for this single item.\"\n }\n ]\n}\n\nRULES:\n1. Do not invent items, prices, or quantities.\n2. If there are no line items found in either the email or the attachment, return an empty array for \"order_items\".\n3. Ensure numerical quantities and unit_prices are returned as clean numbers, not strings. Do not include currency symbols (e.g., return 660, not \"R660\" or \"ZAR 660\"). If a unit price is not provided, return 0.\n4. CUSTOMER EXTRACTION PRIORITY: You must look for the \"company_name\" and \"contact_name\" inside the ATTACHMENT TEXT first (e.g., the letterhead or \"Bill To\" section of the Purchase Order). Use the EMAIL BODY (e.g., email signature or instructions) as a fallback ONLY if these details are completely missing from the attachments.\n5. REQUESTER VS BUYER: Large corporate orders often list a Procurement Buyer (e.g., 'Current Buyer: Chagi') and a specific Requester (e.g., 'Requester/Deliver To: Dreyer, Mr. Petrus' or 'Notes: Req ..._P Dreyer'). You MUST extract the specific Requester as the contact_name. Only default to the Buyer if no Requester is mentioned anywhere in the document.\n6. The \"ATTACHMENT TEXT\" section may contain content from MULTIPLE separate attachments, each clearly marked with a header like \"=== ATTACHMENT X OF Y ===\". Each marked block is a DISTINCT document that may contain its own items, totals, and terms. You MUST extract items from EVERY block listed. Do not stop scanning after one document's \"Total\" or \"Terms\" section \u2014 continue past each \"=== ATTACHMENT ===\" marker and extract that document's items too. The final \"order_items\" array must include items from ALL attachments combined.",
|
|
"passthroughBinaryImages": false
|
|
}
|
|
},
|
|
"id": "1258e927-e791-43e1-82ba-f276ee503b0d",
|
|
"name": "AI Agent",
|
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
"typeVersion": 3.1,
|
|
"position": [
|
|
2112,
|
|
2624
|
|
],
|
|
"retryOnFail": true,
|
|
"waitBetweenTries": 5000
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {
|
|
"hideThinking": true,
|
|
"temperature": 0.1,
|
|
"timeout": 100000,
|
|
"maxRetries": 3
|
|
}
|
|
},
|
|
"id": "5354d113-eebb-4a68-b648-82dd6439fe89",
|
|
"name": "Minimax Chat Model",
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2096,
|
|
2864
|
|
],
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "pdf",
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"options": {
|
|
"joinPages": true,
|
|
"keepSource": "both"
|
|
}
|
|
},
|
|
"id": "674be490-fc4e-479b-b885-4cc280ed3dbf",
|
|
"name": "Extract PDF",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
464,
|
|
1936
|
|
],
|
|
"onError": "continueRegularOutput"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Turn one email item with N binary attachments into N items,\n// each carrying exactly one binary, plus the original email json.\nconst item = $input.first();\nconst binaryKeys = Object.keys(item.binary || {});\n\nif (binaryKeys.length === 0) {\n // no attachments \u2014 pass the email through with an empty marker\n return [{ json: { ...item.json, hasAttachments: false, totalAttachments: 0 }, binary: {} }];\n}\n\nreturn binaryKeys.map((key, index) => ({\n json: {\n ...item.json,\n binaryKey: key,\n fileName: item.binary[key].fileName,\n mimeType: item.binary[key].mimeType,\n fileSize: item.binary[key].fileSize,\n attachmentIndex: index,\n totalAttachments: binaryKeys.length,\n hasAttachments: true\n },\n binary: { [key]: item.binary[key] }\n}));"
|
|
},
|
|
"id": "51bca253-9383-400b-8118-9941cc1373d7",
|
|
"name": "Split Attachments1",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
16,
|
|
2416
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"numberInputs": 7
|
|
},
|
|
"type": "n8n-nodes-base.merge",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1360,
|
|
2544
|
|
],
|
|
"id": "8e4b7968-edb8-4ad2-9370-bbf860d64cea",
|
|
"name": "Merge2"
|
|
},
|
|
{
|
|
"parameters": {},
|
|
"type": "n8n-nodes-base.limit",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-432,
|
|
2416
|
|
],
|
|
"id": "5920ed39-2773-4127-a6ac-2d5747013b7c",
|
|
"name": "Limit"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "fa209857-8e0c-400d-8d18-5cad78d5a988",
|
|
"leftValue": "={{ $json.output.order_items.length }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEquals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
2576,
|
|
2624
|
|
],
|
|
"id": "79a5e452-ff6a-4338-a2a1-b07449024198",
|
|
"name": "If1"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "moveEmail",
|
|
"sourceMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $('Limit').item.json.attributes?.uid || $('Limit').item.json.uid }}",
|
|
"destinationMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Failed",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Failed"
|
|
}
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3568,
|
|
2800
|
|
],
|
|
"id": "ce07cbfd-5159-4333-98a9-f65d13969eba",
|
|
"name": "MoveEmail email1",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"workflowId": {
|
|
"__rl": true,
|
|
"value": "Fm1c24RCfEQt9Qkn",
|
|
"mode": "list",
|
|
"cachedResultUrl": "/workflow/Fm1c24RCfEQt9Qkn",
|
|
"cachedResultName": "WFDASH \u2014 Master Drilling Price Matcher (sub-workflow)"
|
|
},
|
|
"workflowInputs": {
|
|
"mappingMode": "defineBelow",
|
|
"value": {},
|
|
"matchingColumns": [],
|
|
"schema": [],
|
|
"attemptToConvertTypes": false,
|
|
"convertFieldsToString": true
|
|
},
|
|
"options": {
|
|
"waitForSubWorkflow": false
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.executeWorkflow",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
6560,
|
|
2080
|
|
],
|
|
"id": "083d87f5-4684-4394-8386-a8dd0fb020cc",
|
|
"name": "Execute Workflow"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "fca9dae9-453b-4481-8fcb-86a53b051aab",
|
|
"leftValue": "={{ $('Limit').first().json.envelope.sender[0].address }}",
|
|
"rightValue": "masterdrilling",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
6336,
|
|
2032
|
|
],
|
|
"id": "b5da54e3-3288-42a9-9bcc-b05dfdcd2026",
|
|
"name": "If2"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"mailboxes": [
|
|
"INBOX"
|
|
],
|
|
"emailDateRange": {},
|
|
"emailFlags": {
|
|
"flagged": false,
|
|
"seen": false
|
|
},
|
|
"customLabels": {},
|
|
"emailSearchFilters": {},
|
|
"includeParts": [
|
|
"textContent",
|
|
"htmlContent",
|
|
"markdownContent",
|
|
"attachmentsInfo",
|
|
"flags",
|
|
"size",
|
|
"bodyStructure",
|
|
"headers"
|
|
]
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1904,
|
|
1456
|
|
],
|
|
"id": "4bf25239-7d91-4733-ad14-a4d84f992f42",
|
|
"name": "CHECK FOR NEW MAIL",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build the user message for the AI Agent.\n//\n// Inputs:\n// - Aggregate output: { data: [item, item, item] } \u2190 items\n// - Upstream \"Create orders\" output: full parent order\n//\n// Output: { userMessage, target, parentId, items }\n// - userMessage: the assembled prompt string (\u2192 AI Agent)\n// - target: \"order\"\n// - parentId: parent order id (\u2192 PATCH-back to set ai_insights)\n// - items: line item ids (\u2192 PATCH-back loop to set ai_item_insights)\n\nconst data = $json.data;\nif (!data || !Array.isArray(data) || data.length === 0) {\n throw new Error('Aggregate returned no data \u2014 check the upstream Aggregate node');\n}\n\n// Pull the parent from the Create orders node. Rename the node in the\n// expression below if yours is named differently (e.g. \"PocketBase1\").\nconst parent = $('Create orders').first().json;\nif (!parent || !parent.id) {\n throw new Error(\n 'Parent order not found \u2014 check the node name in $(\"Create orders\")',\n );\n}\n\nconst supplierIds = new Set();\nfor (const it of items) {\n if (it.supplier) supplierIds.add(it.supplier);\n}\n\nconst ref = parent.order_number || '(not set)';\n// expand is included only if the parent was fetched with ?expand=\n// \u2014 falls back to the id if not.\nconst companyName = parent.expand?.company?.company || '(not set)';\nconst customerName = parent.expand?.customer?.customer || '(not set)';\n\nlet msg = `Analyse this order and produce insights for the parent and for each line item.\\n\\n`;\nmsg += `## Parent record\\n\\n`;\nmsg += `id: ${parent.id}\\n`;\nmsg += `order_number: ${ref}\\n`;\nmsg += `company_id: ${parent.company || '(not set)'}\\n`;\nmsg += `company_name: ${companyName}\\n`;\nmsg += `customer_id: ${parent.customer || '(not set)'}\\n`;\nmsg += `customer_name: ${customerName}\\n`;\nmsg += `status: ${parent.status || '(not set)'}\\n`;\nmsg += `created: ${parent.created || '(not set)'}\\n`;\nmsg += `notes: ${parent.notes || '(none)'}\\n\\n`;\n\n// \u2500\u2500 Fetch sprint categories from PocketBase \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet categoriesText = '(no active categories)';\ntry {\n const catRes = await $http.request({\n method: 'GET',\n url: '/api/collections/categories/records',\n params: {\n filter: 'is_active = true',\n sort: 'display_order',\n fields: 'id,name,slug',\n perPage: 100,\n },\n });\n if (catRes.response.statusCode === 200) {\n const cats = catRes.data?.items || [];\n if (cats.length > 0) {\n const lines = cats.map(c => ` - ${c.name} (slug: ${c.slug || c.id})`);\n categoriesText = lines.join('\\n');\n }\n }\n} catch (err) {\n // Non-fatal \u2014 the workflow still produces a useful userMessage\n categoriesText = '(category fetch failed \u2014 assign manually in the sprint)';\n}\n\nmsg += `## Line items (${data.length})\\n\\n`;\nfor (const it of data) {\n msg += `- id: ${it.id}\\n`;\n msg += ` description: ${it.description || '(empty)'}\\n`;\n msg += ` sku: ${it.sku || '(not set)'}\\n`;\n msg += ` quantity: ${it.quantity ?? '(not set)'}\\n`;\n msg += ` cost_price: ${it.cost_price ?? '(not set)'}\\n`;\n msg += ` selling_price: ${it.selling_price ?? '(not set)'}\\n`;\n msg += ` supplier_id: ${it.supplier || '(not set)'}\\n\\n`;\n}\n\nreturn [{\n json: {\n userMessage: msg,\n target: 'order',\n parentId: parent.id,\n items: data.map(it => ({ id: it.id })),\n },\n}];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3920,
|
|
2032
|
|
],
|
|
"id": "6c4fb948-b1d9-40ca-a45d-f753bd11b9d9",
|
|
"name": "MESSAGE FOR AI",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"aggregate": "aggregateAllItemData",
|
|
"include": "specifiedFields",
|
|
"fieldsToInclude": "category, collectionId, collectionName, cost_price, created, description, id, markup, matched_price_list_id, quantity, quote, quote_description, quote_number, selling_price, sku, supplier",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.aggregate",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3696,
|
|
2032
|
|
],
|
|
"id": "df65d61d-1f86-468c-b62d-729c7d129f9b",
|
|
"name": "Aggregate",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Split the validated AI output into parent + per-item patches.\n// The Structured Output Parser guarantees the shape, so no\n// defensive parsing is needed here.\n\nconst data = $json.output ?? $json; // parser wraps in .output\nconst upstream = $(\"MESSAGE FOR AI\").first().json;\nconst parentId = upstream.parentId;\nconst expectedItemIds = new Set((upstream.items || []).map(it => it.id));\n\nif (!parentId) {\n throw new Error(\"MESSAGE FOR AI didn't carry parentId\");\n}\n\nconst parentData = data.parent || {};\nconst parentPatch = {\n ...parentData,\n summary: data.summary || parentData.summary || \"\",\n};\n\nconst itemsOut = [];\nfor (const aiItem of data.items || []) {\n if (!aiItem?.id || !expectedItemIds.has(aiItem.id)) continue;\n const { id, ...rest } = aiItem;\n // NOTE: each returned item MUST be { json: {...} } \u2014 n8n validates\n // top-level keys and rejects bare objects. The error you saw\n // (\"Unknown top-level item key: kind\") was because of this.\n itemsOut.push({\n json: {\n kind: \"item\",\n itemId: id,\n aiItemInsights: rest,\n },\n });\n}\n\nreturn [\n { json: { kind: \"parent\", parentId, aiInsights: parentPatch } },\n ...itemsOut,\n];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5664,
|
|
2032
|
|
],
|
|
"id": "46768290-760f-4c73-852d-a48cd7043d93",
|
|
"name": "Parse Ai Resposes",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"promptType": "define",
|
|
"text": "={{ $json.userMessage }}",
|
|
"hasOutputParser": true,
|
|
"needsFallback": true,
|
|
"options": {
|
|
"systemMessage": "You are a quoting and ordering analyst for a parts distributor.\nYour job is to look at one order and produce a\nstructured review that helps the operator decide what to do next.\n\n# Tools you have\n\nYou can query the company's order book and inventory in PocketBase.\nYou have ONE tool per collection. Each tool supports at least `filter`,\n`sort`, and `limit`. Use them.\n\n- pb_quotes \u2014 historical quotes\n- pb_quote_items \u2014 historical quote line items (filter by quote id, sku, or description)\n- pb_orders \u2014 historical orders\n- pb_order_items \u2014 historical order line items (filter by order id, sku, or description)\n- pb_stock \u2014 current stock for a SKU or by description\n- pb_price_list \u2014 cost & sell history (filter by search_text, description, or sku)\n\nEach tool returns PB records. Record ids are 15-character strings like\n`pbc_xxxxx` or `q1w2e3r4t5y6u7i` \u2014 these are the only valid `ref_id`\nvalues. Never invent ids.\n\n# Matching items without a SKU \u2014 DEFAULT BEHAVIOUR\n\nMost new quotes arrive WITHOUT a SKU. The line item only has a\ndescription like \"O19 PPE NOTICE BOARD SIGN 300X300 PVC\". You MUST\nmatch these by description. \"No match\" is not an acceptable answer\nfor an item that has any description text.\n\n## Step 1 \u2014 Build keywords\n\nTake the description, drop noise:\n \"O19 - PPE NOTICE BOARD SIGN 300 X 300 PVC\" \u2192\n \"ppe notice board sign pvc 300\"\n\n## Step 2 \u2014 Search price_list (ALWAYS do this first)\n\n filter: search_text ~ \"ppe notice board\" (try 2-3 word combos)\n\n`search_text` is a pre-computed, lowercased, no-spaces field that\nexists specifically for this. Every price_list record has it.\n\nIf the first keyword combo returns 0 records, try:\n - A different 2-word combo (e.g. \"notice sign\", \"ppe pvc\")\n - A 1-word combo (e.g. \"ppe\", \"pvc\")\n - `description ~ \"ppe\"` (raw description, case-insensitive contains)\n\n## Step 3 \u2014 Verify the candidate\n\nIf you find 1-5 candidates, pick the one whose `description`\nmatches the line item best. If you find 5+, narrow the keywords.\n\n## Step 4 \u2014 Include the match in the output\n\nThe item summary MUST mention the matched price_list record:\n\n \"Matches price list 'O19 - PPE NOTICE BOARD SIGN 300 X 300 PVC'\n (cost R110, sell R165, stock 25, supplier DESIGN STUDIO).\n Source: id 11gkmah2bxgwi6f.\"\n\n## Step 5 \u2014 Only if you have a SKU on the line item\n\nIf (and only if) the line item has a non-empty `sku` field,\nalso try:\n filter: sku = \"<sku>\" (exact, 1 record usually)\n filter: sku_normalised = \"<sku-no-dashes>\" (fallback)\n\nBut description search FIRST, always.\n\n# pb_quote_items + pb_order_items \u2014 matching without SKU\n\nWhen the line item has no SKU, also search historical records by\ndescription text:\n\n filter: description ~ \"<2-3 most specific words from description>\"\n\nCombine the words with `&&`. Example for \"PPE NOTICE BOARD SIGN\":\n\n filter: description ~ \"PPE\" && description ~ \"NOTICE\"\n\nThis catches items that were previously quoted with similar\ndescriptions (e.g. \"115 FLAPPER DISC P60\") even when no SKU\nexists in either the historical quote_item or the price list.\n\n# Input you will receive\n\nThe user message contains:\n- target: \"order\"\n- parent: the full parent record (already includes company, customer,\n line items as `items[]`, totals, dates)\n- items: the same line items as a separate array, for clarity\n\nYou do NOT need to re-fetch the parent \u2014 it's already in the message.\nYou DO need to fetch historical data via tools.\n\n# What to look for \u2014 DOCUMENT level (the parent)\n\nUse the tools to find:\n\n1. Duplicates / near-duplicates \u2014 other quotes or orders for the\n same customer in the last 90 days. If >70% of line items match\n the parent, flag as `duplicate`. If 30-70%, flag as `similar`.\n2. Margin \u2014 if the parent's total margin is <15% and the customer's\n typical margin on past orders is >20%, flag `margin_low`.\n3. Anomalies \u2014 any single line item priced >50% above or below the\n historical average for that SKU across the last 20 records.\n4. Cross-company patterns \u2014 same SKU quoted to a different company\n at a much higher price in the last 30 days (could indicate a\n pricing mistake).\n\n# What to look for \u2014 ITEM level (each line item)\n\nFor each line item in the parent, use the tools to find:\n\n1. Match strategy (see \"Matching items without a SKU\" above for\n the full 5-step process). Description search first, SKU only\n as a fallback.\n2. Last quoted sell price + date for this item to ANY customer\n (via `pb_quote_items`, sort by -created, limit 1).\n3. Last actual sold price + date (via `pb_order_items`, sort by\n -created, limit 1).\n4. Average margin % across the last 20 quotes/sales of this item.\n5. Current stock (via `pb_stock`). Status:\n - `in_stock` if qty > 0 and qty >= parent's order qty\n - `low` if qty > 0 but < parent's order qty\n - `out` if qty === 0\n6. All distinct suppliers who have supplied this item\n\n7. Other quotes for this company with the same item \u2014 use the\n `customer_id` or `company_id` from the parent record to find\n OTHER quotes for the same end customer, then search their\n `quote_items` for items with a similar description. Include:\n - quote_number and quote status\n - the sell price and date for comparison\n Example: \"Also quoted to this company on Q010412 (approved 2026-07-10)\n at R344.05 (same item description, different SKU)\"\n, found via\n the price_list matches. See \"Supplier names\" below for how to\n write the name.\n\n# Supplier names\n\n## Supplier ID \u2192 Name resolution from pb_order_items\n\nThe `pb_order_items` collection stores suppliers as PocketBase relation\nIDs (e.g. `rlhxhaw73dxmsu3`). When you query `pb_order_items` and the\nreturned record has a non-null `supplier` field, you MUST look up the\nreadable name via `pb_suppliers` before including it in your output:\n\n filter: id = \"<supplier_id from order_items>\"\n fields: \"id,suppliername,email\"\n\nUse the `suppliername` field from the response as `supplier_name`\nin the output. Do NOT write the raw ID as the name.\n\nExample \u2014 WRONG (raw ID):\n { \"supplier_id\": \"rlhxhaw73dxmsu3\", \"supplier_name\": \"rlhxhaw73dxmsu3\" }\n\nExample \u2014 CORRECT (resolved via pb_suppliers):\n { \"supplier_id\": \"rlhxhaw73dxmsu3\", \"supplier_name\": \"BEARINGS DIRECT\" }\n\n`supplier_id` in the output is OPTIONAL. Only include it if you\nhave one. `supplier_name` MUST be the human-readable name.\n \u2014 come from price_list, NOT from a tool call\n\nThe `supplier` field on every price_list record is the supplier\nNAME directly (e.g. \"DESIGN STUDIO\", \"BEARINGS DIRECT\", \"SKF SA\").\nYou do NOT need a separate `pb_suppliers` tool to resolve names.\n\nWhen the line item has a price_list match, copy the `supplier`\nfield from that record directly into the response's\n`supplier_name`. The dashboard renders the name fine without an id.\n\nExample price_list match:\n { \"id\": \"...\", \"description\": \"...\", \"supplier\": \"DESIGN STUDIO\", ... }\n\nExample output for the suppliers[] entry:\n { \"supplier_name\": \"DESIGN STUDIO\",\n \"last_cost\": 110,\n \"last_bought_date\": \"2026-07-24\",\n \"times_bought\": 1 }\n\n`supplier_id` in the response is OPTIONAL. Only include it if you\nhave one from the line item itself (`quote_items.supplier`) or\nfrom a separate lookup. Most price_list matches give you the name\nwithout an id, and that's fine.\n\n# Output shape \u2014 PARENT\n\n{\n \"summary\": \"<one short sentence, must reference at least one concrete fact from the data, e.g. 'Possible duplicate of Q-1234' or 'No history for Wescoal on this SKU'>\",\n\n \"parent\": {\n \"summary\": \"<1-3 sentence plain-language verdict>\",\n \"warnings\": [\n {\"type\": \"duplicate|similar|margin_low|price_spike|missing_data\", \"severity\": \"high|medium|low\", \"message\": \"<plain text>\", \"ref_id\": \"<pb record id or null>\"}\n ],\n \"suggestions\": [\n {\"message\": \"<plain text>\", \"ref_id\": \"<pb record id or null>\"}\n ]\n }\n}\n\n# Output shape \u2014 ITEMS\n\nFor each line item in the input, return one entry. The minimum is\n`id` + `summary`; everything else is optional. Keep summaries short\n(1-2 sentences) so the model fits all items in the output budget.\n\n{\n \"items\": [\n {\n \"id\": \"<the order_item id from the input>\",\n \"summary\": \"<1-2 sentences about this item \u2014 what you found, what's missing, anything noteworthy>\"\n }\n ]\n}\n\n# Per-item fields \u2014 OPTIONAL\n\nIf you have specific data and it fits the budget, add any of:\n\n last_quoted_price, last_quoted_date,\n last_sold_price, last_sold_date,\n avg_margin_pct,\n in_stock, stock_status (\"in_stock\" | \"low\" | \"out\"),\n is_stock_item,\n suppliers: [{ supplier_name, supplier_id?, last_cost,\n last_bought_date, times_bought }]\n\n`supplier_name` is REQUIRED if you include a suppliers[] entry\n(see \"Supplier names\" above for where it comes from).\n`supplier_id` is OPTIONAL \u2014 only include it if you have one.\n\nDon't try to fill these for items that genuinely have no data \u2014\nomit the key entirely. The UI is shape-agnostic.\n\n# Display rules\n\nWhen writing text for the operator, ALWAYS prefer human-readable\nidentifiers over raw ids:\n\n- For any supplier, use `supplier_name` \u2014 the price_list record's\n `supplier` field IS the name. No lookup needed.\n- For customer/company, use the name from pb_customers /\n pb_companies, not the id.\n- The structured fields (warnings[].ref_id, suppliers[].supplier_id)\n still take raw ids when you have them \u2014 those are join keys, the\n UI uses them to build navigation links. Don't change those.\n- Only the prose / summary / suggestions text gets the friendly names.\n\n\n\n# Sprint categories \u2014 assign every line item to ONE of these\n\nWhen the operator processes this order, each line item will be\nplaced in one of the following sprint categories. Use the category\nname as a plain string in your output. Do NOT create new categories.\n\nAvailable categories:\n{{ SPRINT_CATEGORIES }}\n\nIMPORTANT: Every line item should be assigned a `category` in the\noutput (e.g. \"Hand Tools\" or \"Adhesives & Sealants\"). If the item\ndoes not match any known category, use the closest match or leave\nthe category blank. Never invent a category not in the list above.\n\n# Hard rules (these are non-negotiable)\n\n- Only use ids, prices, dates, and supplier names returned by the\n tools. Never invent.\n- If a tool returned no data for a field, output `null` (numbers\n and dates) or omit the key entirely (suppliers list, warnings list).\n- The `id` field in each `items[]` entry MUST be one of the item ids\n from the input message. The UI uses it to PATCH the right record.\n- The top-level `summary` and `parent.summary` must reference at\n least one concrete fact from the data (a record id, a price, a\n date, a SKU). No generic platitudes like \"This quote looks fine.\"\n- The `items` array MUST contain one entry per line item from the\n input, in the same order. Same id. If the input has 3 items, the\n output MUST have 3 items. No skipping, even if the line item is\n blank. A blank line item with summary \"no data\" is more useful\n to the operator than a missing entry.\n- For a line item with description but no SKU, you MUST query\n pb_price_list with `search_text ~ <keywords>` before declaring\n no history. You MUST attempt at least 2 different keyword\n combinations before giving up. \"No history\" is never an\n acceptable answer for an item with a description.\n- If you give up on a no-SKU item, the item summary must explain\n WHY (e.g. \"no candidate within 5 of 'O19 PPE NOTICE BOARD'\") so\n the operator can fix the data or reword.\n- supplier_name in any suppliers[] array comes from the `supplier`\n field of a price_list match. NOT from <unknown>. NOT from a\n hallucinated name. NEVER write <unknown> in supplier_name.\n If the line item genuinely has no price_list match AND no\n supplier name from any source, omit the entire suppliers[]\n entry for that item \u2014 do not write <unknown>.\n- supplier_id is OPTIONAL in the response. Don't make one up. Only\n include it if you actually have one (from the line item's\n `supplier` field, or fetched via pb_suppliers if you ever need to).\n- The id of a quote, supplier, customer, or company NEVER appears\n in summary, parent.summary, or suggestions[].message text.\n If you need to mention a record, use the human-readable name\n (e.g. quote_number, supplier_name).\n- Output JSON only. No markdown fences. No prose before or after.\n- `ref_id` in warnings/suggestions must be an actual PB record id\n from a tool response. Use `null` if you don't have one.\n\n# Performance\n\n- Use `limit: 1` whenever you only need the most recent record.\n- Use `limit: 20` for averages and cross-quote patterns.\n- Use parallel tool calls in one turn when you have multiple\n independent queries (e.g. for 5 items, query all 5 stocks in\n parallel rather than one at a time).\n- Aim for 10-30 total tool calls per parent. If you're using more\n than 50, you're probably querying per item what you could query\n in bulk.",
|
|
"maxIterations": 25
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
"typeVersion": 3.1,
|
|
"position": [
|
|
4640,
|
|
2032
|
|
],
|
|
"id": "97f8ca28-f2c8-4673-bc43-cb50e9ef2b61",
|
|
"name": "AI Agent1",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "MiniMax-M2.5",
|
|
"options": {
|
|
"maxTokens": 14000
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4160,
|
|
2272
|
|
],
|
|
"id": "a80fd4cc-da32-45f4-8875-a9eb45d1848b",
|
|
"name": "MiniMax Chat Model",
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"schemaType": "manual",
|
|
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"summary\": { \"type\": \"string\" },\n \"parent\": {\n \"type\": \"object\",\n \"properties\": {\n \"summary\": { \"type\": \"string\" },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"type\": { \"type\": \"string\" },\n \"severity\": { \"type\": \"string\" },\n \"message\": { \"type\": \"string\" },\n \"ref_id\": { \"type\": [\"string\", \"null\"] }\n },\n \"additionalProperties\": true\n }\n },\n \"suggestions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"message\": { \"type\": \"string\" },\n \"ref_id\": { \"type\": [\"string\", \"null\"] }\n },\n \"additionalProperties\": true\n }\n }\n },\n \"additionalProperties\": true\n },\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": { \"type\": \"string\" },\n \"summary\": { \"type\": \"string\" },\n \"last_quoted_price\": { \"type\": [\"number\", \"null\"] },\n \"last_quoted_date\": { \"type\": [\"string\", \"null\"] },\n \"last_sold_price\": { \"type\": [\"number\", \"null\"] },\n \"last_sold_date\": { \"type\": [\"string\", \"null\"] },\n \"avg_margin_pct\": { \"type\": [\"number\", \"null\"] },\n \"in_stock\": { \"type\": \"number\" },\n \"stock_status\": { \"type\": \"string\" },\n \"is_stock_item\": { \"type\": \"boolean\" },\n \"suppliers\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"supplier_id\": { \"type\": [\"string\", \"null\"] },\n \"supplier_name\": { \"type\": \"string\" },\n \"last_cost\": { \"type\": \"number\" },\n \"last_bought_date\": { \"type\": \"string\" },\n \"times_bought\": { \"type\": \"integer\" }\n },\n \"additionalProperties\": true\n }\n }\n },\n \"required\": [\"id\", \"summary\"],\n \"additionalProperties\": true\n }\n }\n },\n \"required\": [\"summary\", \"parent\", \"items\"]\n}",
|
|
"autoFix": true
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
5312,
|
|
2272
|
|
],
|
|
"id": "7f92de6d-dbc6-4b02-b681-2ace19fb712d",
|
|
"name": "Structured Output Parser1",
|
|
"retryOnFail": true,
|
|
"waitBetweenTries": 3000
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {
|
|
"maxTokens": 196608
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5392,
|
|
2480
|
|
],
|
|
"id": "bc92b9cc-d942-47ad-b532-96b04d349750",
|
|
"name": "MiniMax Chat Model1",
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.parentId }}",
|
|
"parameters": {
|
|
"allElements": false
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "fb784378-61fe-4930-b295-79e2f36599db",
|
|
"name": "ai_insights",
|
|
"value": "={{ JSON.stringify($json.aiInsights) }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6112,
|
|
1936
|
|
],
|
|
"id": "1f26cda2-a1e0-4241-b6d3-c693a806f991",
|
|
"name": "UPDATE AI ORDER",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
},
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "order_items",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.itemId }}",
|
|
"parameters": {
|
|
"allElements": true
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "91860763-08b8-4738-97b9-738e1e4d1d0c",
|
|
"name": "=ai_item_insights",
|
|
"value": "={{ JSON.stringify($json.aiItemInsights) }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6112,
|
|
2128
|
|
],
|
|
"id": "f360f43c-fa37-4b52-834f-13d09bb8b70f",
|
|
"name": "UPDATE AI ORDER ITEM",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
},
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.kind }}",
|
|
"rightValue": "parent",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
},
|
|
"id": "d07219a2-64b0-4d3c-97ec-7f2b85d2a8c9"
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "5f3c5956-789d-41cc-b284-56080eb9d0a2",
|
|
"leftValue": "={{ $json.kind }}",
|
|
"rightValue": "item",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals",
|
|
"name": "filter.operator.equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.4,
|
|
"position": [
|
|
5888,
|
|
2032
|
|
],
|
|
"id": "38e87e74-ace7-4483-8a62-cb87c97c797a",
|
|
"name": "Switch",
|
|
"disabled": true
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_suppliers",
|
|
"resource": "suppliers",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4416,
|
|
2272
|
|
],
|
|
"id": "117145a6-3019-42b7-8ae6-d25c813adb42",
|
|
"name": "pb_suppliers",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_price_list",
|
|
"resource": "price_list",
|
|
"parameters": {
|
|
"allElements": false,
|
|
"fields": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Field_Names', ``, 'string') }}",
|
|
"filter": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Filter', ``, 'string') }}"
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4544,
|
|
2272
|
|
],
|
|
"id": "9dfb1316-692b-45f2-8b1f-4fb63a56d8f6",
|
|
"name": "pb_price_list",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_order_items",
|
|
"resource": "order_items",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4672,
|
|
2272
|
|
],
|
|
"id": "4be8f708-b07e-48db-a351-8c04f9621284",
|
|
"name": "pb_order_items",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_stock",
|
|
"resource": "stock_items",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4800,
|
|
2272
|
|
],
|
|
"id": "e5abdc3c-87e8-4281-b773-149ac0b4f065",
|
|
"name": "pb_stock",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_orders",
|
|
"resource": "orders",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4928,
|
|
2272
|
|
],
|
|
"id": "5f1271d5-d0b9-4a9e-b81c-dc83030823b1",
|
|
"name": "pb_orders",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_quote_items",
|
|
"resource": "quote_items",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5056,
|
|
2272
|
|
],
|
|
"id": "b6e1581c-003b-4972-b110-99b380303080",
|
|
"name": "pb_quote_items",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"descriptionType": "manual",
|
|
"toolDescription": "pb_quotes",
|
|
"resource": "quotes",
|
|
"parameters": {},
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttpTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5184,
|
|
2272
|
|
],
|
|
"id": "6b123fb9-427f-4886-a739-9e82bed3e187",
|
|
"name": "pb_quotes",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "deepseek-v4-flash",
|
|
"options": {}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4288,
|
|
2272
|
|
],
|
|
"id": "37de04bb-36f3-470f-a22b-117183a00f1b",
|
|
"name": "Ollama Chat Model",
|
|
"credentials": {
|
|
"ollamaApi": {
|
|
"id": "7zP3EPakfo3Tns7J",
|
|
"name": "Ollama account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rule": {
|
|
"interval": [
|
|
{
|
|
"field": "minutes"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"type": "n8n-nodes-base.scheduleTrigger",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
-2128,
|
|
1456
|
|
],
|
|
"id": "92ba5d2b-10b6-4ebf-b1e2-360cf05f0d3b",
|
|
"name": "Schedule Trigger"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "a01bff61-3b3a-4fb4-940b-ff8dedd96e73",
|
|
"name": "contact_name",
|
|
"value": "={{ $('AI Agent').item.json.output.order_info.contact_name }}\n",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "34065da3-fc4d-40bf-9032-0ce283a9b892",
|
|
"name": "email_sender",
|
|
"value": "={{ $('AI Agent').item.json.output.order_info.sender_email }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "a467a859-227f-4bbd-83c7-e3ef41660bf4",
|
|
"name": "customer",
|
|
"value": "={{ $('Search customers').item.json?.id || 'xmd68ps32k1x5fu' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "57b896df-4725-4542-bfbc-3308014f5c7e",
|
|
"name": "email_body",
|
|
"value": "={{ ($('Limit').item.json.textContent || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "f45c1fa7-34b4-4994-997e-3747d0e6aecd",
|
|
"name": "email_message_id",
|
|
"value": "={{ $('Limit').item.json.headers['message-id'] }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "cccce8aa-78ea-414e-914f-945194ff9056",
|
|
"name": "description",
|
|
"value": "={{ $('AI Agent').item.json.output.order_items[0].description }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "2546ea57-e8f1-4053-af0d-0c0bba156d57",
|
|
"name": "email_subject",
|
|
"value": "={{ $('Limit').item.json.envelope.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "5ea60f5f-2b1d-4464-a030-1bfd38592a38",
|
|
"name": "order_number",
|
|
"value": "={{ $('AI Agent').item.json.output.order_info.rfq_number || $('Limit').item.json.subject || 'NO-REF' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "7b451b77-966e-46dc-aafe-f03478182998",
|
|
"name": "company",
|
|
"value": "={{ $('Search customers').item.json?.company || 'fufdg0k9q128wzv' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "b248cb18-bd40-4dfc-adb2-7b64f3f987f3",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "b253f84b-7286-4e5f-a9b0-ec0c770c2684",
|
|
"name": "rep",
|
|
"value": "={{ $json.rep }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "65376803-9e69-4f6f-996e-50875fe659af",
|
|
"name": "Create orders",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3024,
|
|
1952
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "downloadAttachment",
|
|
"mailboxPath": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $json.uid }}",
|
|
"allAttachments": true,
|
|
"includeInlineAttachments": true
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-208,
|
|
2416
|
|
],
|
|
"id": "29804fcc-7dd3-4cfe-a7b3-1a3470acbd86",
|
|
"name": "Download ATTACHMENT",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"pageRange": "="
|
|
},
|
|
"type": "@bitovi/n8n-nodes-pdf-to-png.pdfToPng",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
912,
|
|
1856
|
|
],
|
|
"id": "3757a091-bcf3-4a3a-93ac-7de1c458561c",
|
|
"name": "PDF to PNG"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "c7872cb1-6bcc-4021-9c0b-3d90b337375a",
|
|
"leftValue": "={{ $json.text }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
688,
|
|
1936
|
|
],
|
|
"id": "b625acdd-e061-4a07-a230-2d975010969d",
|
|
"name": "If3"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "image",
|
|
"modelId": {
|
|
"__rl": true,
|
|
"value": "MiniMax-M3",
|
|
"mode": "list",
|
|
"cachedResultName": "MiniMax-M3"
|
|
},
|
|
"text": "=What's in this image? if its more than one order or image can you combinde thye contents into 1 and just add the order number next to the items on the order that it refrences to ",
|
|
"inputType": "binary",
|
|
"options": {
|
|
"maxTokens": 524288
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.anthropic",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1136,
|
|
1856
|
|
],
|
|
"id": "af384a3c-5189-4cd9-a472-0d113158dfee",
|
|
"name": "Analyze image",
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "jLsa9I3U2W6vqzeE",
|
|
"name": "Anthropic account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"formBinaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3920,
|
|
1424
|
|
],
|
|
"id": "3fd765c7-32e5-4240-8206-59d7d16919b6",
|
|
"name": "Update orders",
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Combine All Extracted Texts\"\nconst items = $input.all();\n\n// Extract text from each item (handles both nested content arrays and flat text)\nconst allTexts = items.map(item => {\n if (item.json.content && item.json.content[0]?.text) {\n return item.json.content[0].text;\n }\n return item.json.text || '';\n}).filter(Boolean);\n\n// Combine them into one single string with a clean separator\nreturn [{\n json: {\n aggregatedText: allTexts.join('\\n\\n--- [ NEXT ATTACHMENT ] ---\\n\\n')\n }\n}];"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1728,
|
|
2624
|
|
],
|
|
"id": "fdb4419c-7fc8-42d8-ba55-0e2e4a732786",
|
|
"name": "Code in JavaScript4"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "moveEmail",
|
|
"sourceMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $('Limit').first().json.uid }}",
|
|
"destinationMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Completed",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Completed"
|
|
}
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6560,
|
|
1664
|
|
],
|
|
"id": "c82d9996-f8e5-4437-80fd-74b24390bac4",
|
|
"name": "MoveEmail email2",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "setEmailFlags",
|
|
"mailboxPath": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Completed",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Completed"
|
|
},
|
|
"emailUid": "={{ $('Limit').first().json.uid }}",
|
|
"flags": {
|
|
"\\Seen": true
|
|
}
|
|
},
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6784,
|
|
1664
|
|
],
|
|
"id": "ade33045-d40e-4800-b65a-ec345b2e25ad",
|
|
"name": "SetEmailFlags email",
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "moveEmail",
|
|
"sourceMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX",
|
|
"cachedResultUrl": ""
|
|
},
|
|
"emailUid": "={{ $json.attributes?.uid || $json.uid }}",
|
|
"destinationMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Bounce&Supplier",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Bounce&Supplier"
|
|
}
|
|
},
|
|
"id": "3293f87c-4e33-40a2-845f-914cc8b3bc05",
|
|
"name": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-208,
|
|
480
|
|
],
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"fromEmail": "ORDERS@WFSALES.CO.ZA",
|
|
"toEmail": "CONAR@WFSALES.CO.ZA",
|
|
"subject": "CHECK THIS BOUNCE OR SUPPLIER MAIL",
|
|
"html": "={{ $json.envelope.from[0].name }}\n{{ $json.envelope.from[0].address }}\n\n{{ $json.htmlContent }}",
|
|
"options": {}
|
|
},
|
|
"type": "n8n-nodes-base.emailSend",
|
|
"typeVersion": 2.1,
|
|
"position": [
|
|
-432,
|
|
416
|
|
],
|
|
"id": "2e4e5b0c-706c-442e-9b63-853b47581a12",
|
|
"name": "Send an Email",
|
|
"webhookId": "f2120f43-da70-4fea-99b1-f0f53487e7f1",
|
|
"credentials": {
|
|
"smtp": {
|
|
"id": "qnkWLVAGkq9H7jT5",
|
|
"name": "PO_WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// EDIT: add supplier domains to this list, or swap for a live\n// pb_suppliers lookup later\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst supplierDomains = ['festo.com'];\n\n// PO number pattern: common South African / industrial formats\n// Matches things like ZA10PO26010307, PO-2026-08-014, PO2026001, etc.\nconst PO_PATTERN = /\\b(?:PO[-\\s]?\\d{4,}|ZA\\d+PO\\d+)\\b/gi;\n\nconst items = $input.all();\n\nreturn items.map(item => {\n const j = item.json;\n const sender = (j.envelope?.sender?.[0]?.address || j.from?.text || '').toLowerCase();\n const subject = (j.envelope?.subject || j.subject || '').toLowerCase();\n const bodyText = (j.text || j.textPlain || j.body || '').toString();\n const headers = j.headers || {};\n\n const contentType = (headers['content-type'] || headers['Content-Type'] || '').toString().toLowerCase();\n const autoSubmittedHeader= (headers['auto-submitted'] || headers['Auto-Submitted'] || '').toString().toLowerCase();\n const precedence = (headers['precedence'] || headers['Precedence'] || '').toString().toLowerCase();\n\n // \u2500\u2500 1. CONFIRMED BOUNCE \u2014 RFC 3464 delivery-status report \u2500\u2500\n const isConfirmedBounce = contentType.includes('report-type=delivery-status');\n\n // \u2500\u2500 2. CONFIRMED SUPPLIER \u2014 domain match \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const senderDomain = (sender.split('@')[1] || '');\n const isConfirmedSupplier = !isConfirmedBounce &&\n supplierDomains.some(d => senderDomain.endsWith(d));\n\n // \u2500\u2500 3. MULTI-ORDER \u2014 multiple PDFs OR multiple PO numbers \u2500\u2500\n // Count PDF attachments\n const attachments = Array.isArray(j.attachments) ? j.attachments : [];\n const pdfCount = attachments.filter(a =>\n (a.contentType || a.type || '').toLowerCase().includes('pdf')\n ).length;\n\n // Count distinct PO numbers across subject + body\n const subjectPOs = (subject.match(PO_PATTERN) || []);\n const bodyPOs = (bodyText.match(PO_PATTERN) || []);\n const allPOs = [...new Set([...subjectPOs, ...bodyPOs].map(p => p.toUpperCase()))];\n\n const isMultiOrder = !isConfirmedBounce && !isConfirmedSupplier &&\n (pdfCount > 1 || allPOs.length > 1);\n\n // \u2500\u2500 4. WEAK SIGNALS \u2014 hand off to AI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const weakBounceSignal =\n sender.includes('mailer-daemon') ||\n sender.includes('postmaster') ||\n subject.includes('delivery failed') ||\n subject.includes('undeliverable') ||\n subject.includes('returning message to sender') ||\n subject.includes('out of office') ||\n subject.includes('automatic reply') ||\n (autoSubmittedHeader && autoSubmittedHeader !== 'no') ||\n precedence.includes('bulk') || precedence.includes('auto_reply');\n\n // \u2500\u2500 ROUTE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n let route = 'customer';\n if (isConfirmedBounce) route = 'bounce';\n else if (isConfirmedSupplier) route = 'supplier';\n else if (isMultiOrder) route = 'multi_order';\n else if (weakBounceSignal) route = 'uncertain';\n\n return { json: { ...j, route, _debug: { pdfCount, allPOs } } };\n});"
|
|
},
|
|
"id": "dab6ee17-4d4b-4278-9d41-f59c9a281bfc",
|
|
"name": "Classify Email (Deterministic)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1680,
|
|
1456
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "r-bounce",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "bounce",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "r-supplier",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "supplier",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "r-multi",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "multi_order",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "r-uncertain",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "uncertain",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "bc725194-0ec7-4128-a5e2-849ee1f35320",
|
|
"name": "Route Email",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
-1456,
|
|
1408
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "MiniMax-M2.1",
|
|
"options": {
|
|
"maxTokens": 10,
|
|
"temperature": 0.1,
|
|
"timeout": 30000,
|
|
"maxRetries": 2
|
|
}
|
|
},
|
|
"id": "9e6ff244-47a2-415e-b669-90206a8a16a4",
|
|
"name": "Cheap Classifier Model (Minimax)",
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1168,
|
|
2400
|
|
],
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"promptType": "define",
|
|
"text": "=Sender: {{ $json.envelope?.sender?.[0]?.address || $json.from?.text }}\nSubject: {{ $json.envelope?.subject || $json.subject }}\nBody snippet: {{ ($json.text || $json.textPlain || '').toString().substring(0,300) }}",
|
|
"messages": {
|
|
"messageValues": [
|
|
{
|
|
"message": "You classify inbound emails for a company's sales quotes inbox. Reply with ONLY one word, no punctuation: BOUNCE, SUPPLIER, or CUSTOMER.\nBOUNCE = an automated delivery failure, out-of-office, or auto-reply notice \u2014 not a real message from a person.\nSUPPLIER = a message from one of our own suppliers (order confirmations, invoices, stock/backorder updates) \u2014 not a customer request.\nCUSTOMER = anything else, including quote requests, replies to quotes, or order queries from a customer."
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"id": "bc3180fe-ea37-42f2-b934-e1857b7b0ffb",
|
|
"name": "AI Classify Email (cheap)",
|
|
"type": "@n8n/n8n-nodes-langchain.chainLlm",
|
|
"typeVersion": 1.5,
|
|
"position": [
|
|
-1232,
|
|
2176
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const items = $input.all();\nreturn items.map(item => {\n const raw = (item.json.text || item.json.output || item.json.response || '').toString().trim().toUpperCase();\n let route = 'customer';\n if (raw.includes('BOUNCE')) route = 'bounce';\n else if (raw.includes('SUPPLIER')) route = 'supplier';\n return { json: { ...item.json, route } };\n});"
|
|
},
|
|
"id": "18fd69e8-8b3d-4f80-8feb-d7dd715e3d44",
|
|
"name": "Parse AI Classification",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-880,
|
|
2176
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "ai-bounce",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "bounce",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "ai-supplier",
|
|
"leftValue": "={{ $json.route }}",
|
|
"rightValue": "supplier",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "c0293379-2505-438d-bac9-1796c48f737b",
|
|
"name": "Route Email (AI Result)",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
-656,
|
|
2160
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "downloadAttachment",
|
|
"mailboxPath": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $json.uid }}",
|
|
"allAttachments": true,
|
|
"includeInlineAttachments": true
|
|
},
|
|
"id": "0a0715c2-1bbc-42c6-ba58-04a2bcfc461b",
|
|
"name": "Download ATTACHMENT (Multi)",
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1168,
|
|
1456
|
|
],
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const item = $input.first();\nconst meta = $('CHECK FOR NEW MAIL').item.json;\nconst subject = meta.envelope?.subject || meta.subject || \"\";\nconst bodyText = (meta.textContent || meta.text || \"\").toString();\nconst binary = item.binary || {};\n\n// Recover the real customer if this looks like an internal forward (e.g. laura@wfsales.co.za forwarding a Master Drilling PO)\nlet fromEmail = meta.envelope?.sender?.[0]?.address || \"\";\nlet fromName = meta.envelope?.sender?.[0]?.name || \"\";\nif (/@wfsales\\.co\\.za$/i.test(fromEmail)) {\n const m = bodyText.match(/From:\\s*([^<\\n]+)<([^>]+)>/i);\n if (m) {\n fromName = m[1].trim();\n fromEmail = m[2].trim();\n }\n}\nconst toEmail = meta.envelope?.to?.[0]?.address || \"\";\nconst messageId = meta.headers?.[\"message-id\"] || meta.headers?.[\"Message-Id\"] || \"\";\n\n// Same PO pattern as \"Classify Email (Deterministic)\" so detection stays consistent\nconst PO_PATTERN = /\\b(?:PO[-\\s]?\\d{4,}|ZA\\d+PO\\d+)\\b/gi;\nconst subjectPOs = subject.match(PO_PATTERN) || [];\nconst bodyPOs = bodyText.match(PO_PATTERN) || [];\nconst poNumbers = [...new Set([...subjectPOs, ...bodyPOs].map(p => p.toUpperCase().replace(/\\s+/g, \"\")))];\n\nif (poNumbers.length === 0) {\n return [{\n json: {\n needs_review: true,\n reason: \"no PO numbers found in subject/body despite multi_order routing\",\n subject,\n from_email: fromEmail,\n from_name: fromName,\n to_email: toEmail,\n body_text: bodyText,\n message_id: messageId,\n },\n binary,\n }];\n}\n\n// Group attachments to the PO number(s) their filename mentions\nconst groups = {};\nfor (const po of poNumbers) groups[po] = {};\nconst unmatched = [];\n\nfor (const key of Object.keys(binary)) {\n const bin = binary[key];\n const fname = (bin.fileName || \"\").toUpperCase().replace(/\\s+/g, \"\");\n const matchedPo = poNumbers.find(po => fname.includes(po));\n if (matchedPo) {\n groups[matchedPo][key] = bin;\n } else {\n unmatched.push(key);\n }\n}\n\nreturn poNumbers.map(po => ({\n json: {\n po_number: po,\n subject: `${po} \\u2014 ${subject}`,\n from_email: fromEmail,\n from_name: fromName,\n to_email: toEmail,\n body_text: bodyText,\n message_id: `${messageId}-${po}`,\n needs_review: false,\n },\n binary: groups[po],\n}));"
|
|
},
|
|
"id": "fa6b7c31-8e7b-4b03-a68a-69b41d2eb6d3",
|
|
"name": "Group attachments by PO",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-880,
|
|
1456
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "9e8b5d6e-ea11-414c-b4d2-7035ad6ed96a",
|
|
"leftValue": "={{ $json.needs_review }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "notEquals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "579b92e8-f5fc-4a76-8f04-03e9e56a1a95",
|
|
"name": "Has valid orders?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
-656,
|
|
1456
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "dbfb84ab-b929-486c-9487-3aec267e78b0",
|
|
"name": "Per-order loop",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
-432,
|
|
1312
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const item = $input.first();\nconst j = item.json;\nreturn [{\n json: {\n po_number: j.po_number,\n subject: j.subject,\n text: j.body_text,\n textContent: j.body_text,\n envelope: {\n subject: j.subject,\n sender: [{ address: j.from_email, name: j.from_name }],\n to: [{ address: j.to_email }],\n },\n headers: { \"message-id\": j.message_id },\n },\n binary: item.binary,\n}];"
|
|
},
|
|
"id": "f956ad2a-2034-4f17-bce4-f37ceeaaddff",
|
|
"name": "Limit (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-208,
|
|
704
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Turn one order item with N binary attachments into N items,\n// each carrying exactly one binary, plus the original order json.\nconst item = $input.first();\nconst binaryKeys = Object.keys(item.binary || {});\n\nif (binaryKeys.length === 0) {\n return [{ json: { ...item.json, hasAttachments: false, totalAttachments: 0 }, binary: {} }];\n}\n\nreturn binaryKeys.map((key, index) => ({\n json: {\n ...item.json,\n binaryKey: key,\n fileName: item.binary[key].fileName,\n mimeType: item.binary[key].mimeType,\n fileSize: item.binary[key].fileSize,\n attachmentIndex: index,\n totalAttachments: binaryKeys.length,\n hasAttachments: true\n },\n binary: { [key]: item.binary[key] }\n}));"
|
|
},
|
|
"id": "bf5deb63-1259-45ff-82d6-d6828712de29",
|
|
"name": "Split Attachments1 (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
16,
|
|
704
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "application/pdf",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
},
|
|
"id": "b737fbd9-7eec-4df0-b168-bbb410c89993"
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "db298d9c-f9cd-4074-8668-c2e04ffe269c",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/csv",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "1d14db84-b83f-49d6-ac5d-f5f6e073fa4f",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/xls",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "65b2d52e-1965-4004-b244-9b815b4a2588",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "text/xlsx",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "ccc71a40-1399-441c-b166-9c2e19b1d723",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "=text/text",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "e1f86572-8d48-4e4b-beea-ab50c8aeaaaa",
|
|
"leftValue": "={{ $('Limit (Multi)').item.json.text || $('Limit (Multi)').item.json.textContent }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "f22c5556-91e4-416b-a003-3fe4ee573a36",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "image/png",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "393b3c7a-1aa7-48f8-bae7-70018b7e8fc0",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "image/jpeg",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "8e6cc1e9-7469-4ba9-8035-84f8d684add4",
|
|
"leftValue": "={{ $json.mimeType }}",
|
|
"rightValue": "application/octet-stream",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"allMatchingOutputs": true
|
|
}
|
|
},
|
|
"id": "40d530e9-48d0-4cf4-a8cd-5aba8fb46e87",
|
|
"name": "Switch1 (Multi)",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
240,
|
|
592
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "pdf",
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"options": {
|
|
"joinPages": true,
|
|
"keepSource": "both"
|
|
}
|
|
},
|
|
"id": "2ecc632f-7099-42fd-8016-8074f4eeda06",
|
|
"name": "Extract PDF (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
464,
|
|
128
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "b4efd17b-967f-4c5a-957a-5f4c93a71c94",
|
|
"leftValue": "={{ $json.text }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "58ba8e39-80b7-41d2-b1f8-efac911a3e36",
|
|
"name": "If3 (Multi)",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
688,
|
|
128
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"pageRange": "="
|
|
},
|
|
"id": "1eaf6227-00bd-4b22-9239-77ed2837cfdf",
|
|
"name": "PDF to PNG (Multi)",
|
|
"type": "@bitovi/n8n-nodes-pdf-to-png.pdfToPng",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
912,
|
|
48
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "image",
|
|
"modelId": {
|
|
"__rl": true,
|
|
"value": "MiniMax-M3",
|
|
"mode": "list",
|
|
"cachedResultName": "MiniMax-M3"
|
|
},
|
|
"text": "What's in this image? if its more than one order or image can you combinde thye contents into 1 and just add the order number next to the items on the order that it refrences to ",
|
|
"inputType": "binary",
|
|
"options": {
|
|
"maxTokens": 524288
|
|
}
|
|
},
|
|
"id": "9cf4c349-bb57-4d0d-aa3b-b42bb4322a97",
|
|
"name": "Analyze image (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.anthropic",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1136,
|
|
48
|
|
],
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "jLsa9I3U2W6vqzeE",
|
|
"name": "Anthropic account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "517a879c-bfd8-4a6f-b29f-74db165d62b0",
|
|
"name": "Extract from CSV1 (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
336
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xls",
|
|
"options": {}
|
|
},
|
|
"id": "b612b79b-8723-4cc5-a5d2-dd43e50564fe",
|
|
"name": "Extract from XLS1 (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
528
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "xlsx",
|
|
"options": {}
|
|
},
|
|
"id": "0e932fde-f4f6-4e27-93e8-d21001207f27",
|
|
"name": "Extract from XLSX1 (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
720
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "text",
|
|
"options": {}
|
|
},
|
|
"id": "f4e8bdf3-5811-4b84-8234-02a1ca995fc2",
|
|
"name": "Extract from TXT1 (Multi)",
|
|
"type": "n8n-nodes-base.extractFromFile",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1136,
|
|
912
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "image",
|
|
"modelId": {
|
|
"__rl": true,
|
|
"value": "MiniMax-M3",
|
|
"mode": "list",
|
|
"cachedResultName": "MiniMax-M3"
|
|
},
|
|
"inputType": "binary",
|
|
"binaryPropertyName": "={{ $json.binaryKey }}",
|
|
"options": {}
|
|
},
|
|
"id": "6d01cb20-d46c-47b3-bd1a-b3bfc6ece878",
|
|
"name": "Analyze image1 (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.anthropic",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
912,
|
|
1200
|
|
],
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "jLsa9I3U2W6vqzeE",
|
|
"name": "Anthropic account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Normalize Image Text\"\nconst item = $input.first();\nconst extractedText = item.json.content?.[0]?.text || '';\nreturn [{\n json: {\n ...item.json,\n text: extractedText\n }\n}];"
|
|
},
|
|
"id": "c1df2c45-a49b-4988-a800-12aa3f67064f",
|
|
"name": "Code in JavaScript3 (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1136,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Combine All Extracted Texts\"\nconst items = $input.all();\n\nconst allTexts = items.map(item => {\n let t;\n if (item.json.content && item.json.content[0]?.text) {\n t = item.json.content[0].text;\n } else {\n t = item.json.text || '';\n }\n // Strip stray curly braces (common in free-form AI vision output) so they\n // don't break n8n's {{ }} expression parsing further downstream.\n return t.replace(/[{}]/g, '');\n}).filter(Boolean);\n\nreturn [{\n json: {\n aggregatedText: allTexts.join('\\n\\n--- [ NEXT ATTACHMENT ] ---\\n\\n')\n }\n}];"
|
|
},
|
|
"id": "e982562d-b370-4dc1-9a6a-b065a25290f4",
|
|
"name": "Code in JavaScript4 (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1360,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"promptType": "define",
|
|
"text": "=Email Subject: {{ $('Limit (Multi)').item.json.envelope.subject }}\n\nSender Email: {{ $('Limit (Multi)').item.json.envelope.sender[0].address }}\n\nRecipient Email: {{ $('Limit (Multi)').item.json.envelope.to[0].address }}\n\n--- EMAIL BODY ---\n{{ $('Limit (Multi)').item.json.textContent }}\n\n\n--- ORDER DETAILS ---\n\n\n\n--- ATTACHMENT TEXT (ALL PDFs COMBINED) ---\n{{ $json.aggregatedText }}\n\n\n\n",
|
|
"hasOutputParser": true,
|
|
"options": {
|
|
"systemMessage": "You are an expert procurement and data extraction AI. Your task is to process incoming orders by analyzing both the email body and any attached document text.\n\nYou must reconcile the information. For example, if the email says \"Please quote the attached 5 items,\" you must extract the company details from the email signature and the 5 items from the attachment text.\n\nExtract the data and return it STRICTLY as a valid JSON object matching the exact structure below. Do not include markdown formatting (like ```json), conversational text, or explanations. Only return the JSON.\n\nJSON STRUCTURE:\n{\n \"order_info\": {\n \"company_name\": \"Extract the buyer/end-customer company name.\",\n \"contact_name\": \"Extract the actual End-User or Requester of the goods (Look for fields like 'Requester', 'Deliver To', 'Attention', or names in the Notes). Do NOT use the 'Buyer' or 'Created By' name if a Requester is listed.\",\n \"sender_email\": \"Extract the email address of the person requesting the quote.\",\n \"rfq_number\": \"Extract any visible RFQ, Reference, or Order number provided by the customer. Leave empty if none.\",\n \"notes\": \"Summarize any specific instructions, delivery requirements, or context from the email body (e.g., 'Urgent delivery required').\"\n },\n \"order_items\": [\n {\n \"description\": \"The exact name, SKU, or description of the requested item.\",\n \"quantity\": 1,\n \"unit_price\": 0.00,\n \"notes\": \"Any specific unit of measure (e.g., 'meters', 'kg') or specific notes for this single item.\"\n }\n ]\n}\n\nRULES:\n1. Do not invent items, prices, or quantities.\n2. If there are no line items found in either the email or the attachment, return an empty array for \"order_items\".\n3. Ensure numerical quantities and unit_prices are returned as clean numbers, not strings. Do not include currency symbols (e.g., return 660, not \"R660\" or \"ZAR 660\"). If a unit price is not provided, return 0.\n4. CUSTOMER EXTRACTION PRIORITY: You must look for the \"company_name\" and \"contact_name\" inside the ATTACHMENT TEXT first (e.g., the letterhead or \"Bill To\" section of the Purchase Order). Use the EMAIL BODY (e.g., email signature or instructions) as a fallback ONLY if these details are completely missing from the attachments.\n5. REQUESTER VS BUYER: Large corporate orders often list a Procurement Buyer and a specific Requester. Prefer the Requester as contact_name when both are present."
|
|
}
|
|
},
|
|
"id": "db98c936-7ce4-4d17-a932-b6c481c9262a",
|
|
"name": "AI Agent (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
"typeVersion": 2.2,
|
|
"position": [
|
|
1616,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {
|
|
"hideThinking": true,
|
|
"temperature": 0.1,
|
|
"timeout": 100000,
|
|
"maxRetries": 3
|
|
}
|
|
},
|
|
"id": "155e8f21-9b7f-4911-b129-45554c6159ad",
|
|
"name": "Minimax Chat Model (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1600,
|
|
1024
|
|
],
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsonSchemaExample": "{\n \"order_info\": {\n \"company_name\": \"Harmony\",\n \"contact_name\": \"Mark Wilkie\",\n \"sender_email\": \"davidels1986@gmail.com\",\n \"rfq_number\": \"PO-20260604-004\",\n \"notes\": \"Please quote on Isherwood letterhead\"\n },\n \"order_items\": [\n {\n \"description\": \"TURBO END+ SUPER TOUGH COUNTERSINK 14-60MM\",\n \"quantity\": 2,\n \"unit_price\": 0.00,\n \"notes\": \"SKU: JEIETCSK-60\"\n }\n ]\n}",
|
|
"autoFix": true
|
|
},
|
|
"id": "17a486c2-4100-4f25-aa57-642f828f7a0b",
|
|
"name": "Structured Output Parser (Multi)",
|
|
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
1728,
|
|
1024
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "f901dce1-4cf0-4e4c-be76-2177b8b3e517",
|
|
"leftValue": "={{ $json.output.order_items.length }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEquals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {}
|
|
},
|
|
"id": "d1db0951-dd17-4851-85ff-c5722f20f635",
|
|
"name": "If1 (Multi)",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
2224,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "customers",
|
|
"parameters": {
|
|
"elementsPerPage": 12,
|
|
"filter": "={{ \n (() => {\n const name = $('AI Agent (Multi)').item.json.output.order_info.contact_name || '';\n const email = $('AI Agent (Multi)').item.json.output.order_info.order_info?.sender_email || $('AI Agent (Multi)').item.json.output.order_info.sender_email || '';\n \n const nameKeywords = name.replace(/mr\\.|mrs\\.|ms\\.|mr|mrs|ms/gi, '').split(/[\\s,]+/).filter(w => w.length > 2);\n const nameQuery = nameKeywords.length > 0 ? nameKeywords.map(w => `customer ~ '${w}'`).join(' && ') : '';\n \n const emailQuery = email ? `email ~ '${email}'` : '';\n \n if (!nameQuery && !emailQuery) {\n return \"id = 'xmd68ps32k1x5fu'\";\n }\n \n return [nameQuery, emailQuery].filter(Boolean).map(q => `(${q})`).join(' || ');\n })()\n}}"
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "ae289c8f-b649-424b-be87-d422815642a2",
|
|
"name": "Search customers (Multi)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2576,
|
|
688
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "9a68ceb0-89fa-43ea-9b38-238b9c9cb02c",
|
|
"name": "contact_name",
|
|
"value": "={{ $('AI Agent (Multi)').item.json.output.order_info.contact_name }}\n",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "c6190435-249d-4150-a435-f0a3ed9a4a26",
|
|
"name": "email_sender",
|
|
"value": "={{ $('AI Agent (Multi)').item.json.output.order_info.sender_email }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "7bb6d032-2683-44b6-80a9-18c6fce371ff",
|
|
"name": "customer",
|
|
"value": "={{ $('Search customers (Multi)').item.json?.id || 'xmd68ps32k1x5fu' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "f6772599-5d9f-41e1-a08c-a4bbce5eff26",
|
|
"name": "email_body",
|
|
"value": "={{ ($('Limit (Multi)').item.json.textContent || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "fe222f30-e88d-4284-a025-c989f31dfaca",
|
|
"name": "email_message_id",
|
|
"value": "={{ $('Limit (Multi)').item.json.headers['message-id'] }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "5353b98b-e37a-4cc0-a394-2be8396615fb",
|
|
"name": "description",
|
|
"value": "={{ $('AI Agent (Multi)').item.json.output.order_items[0].description }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "d694b5ac-323c-4935-8f4e-6e39a828daa3",
|
|
"name": "email_subject",
|
|
"value": "={{ $('Limit (Multi)').item.json.envelope.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "681a3e58-2a33-455a-906a-1ced89049823",
|
|
"name": "order_number",
|
|
"value": "={{ $('AI Agent (Multi)').item.json.output.order_info.rfq_number || $('Limit (Multi)').item.json.po_number || 'NO-REF' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "a8be8f38-a32e-4bae-92dd-8e0fcc3b3c63",
|
|
"name": "company",
|
|
"value": "={{ $('Search customers (Multi)').item.json?.company || 'fufdg0k9q128wzv' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "88728789-c85c-49f6-81c5-2d6782ebe4f5",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "bf66a8fe-a4e3-4787-a51f-44700ce18097",
|
|
"name": "Create orders (Multi)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2800,
|
|
688
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Code node: \"Split Order Items\"\nconst orderId = $('Create orders (Multi)').item.json.id;\nconst items = $('AI Agent (Multi)').item.json.output.order_items;\n\nreturn items.map(item => ({\n json: {\n order: orderId,\n description: item.description,\n quantity: item.quantity,\n selling_price: item.unit_price,\n notes: item.notes\n }\n}));\n"
|
|
},
|
|
"id": "3c8f445d-80b5-4d88-85af-901cd213e0ce",
|
|
"name": "Code in JavaScript2 (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3024,
|
|
288
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "order_items",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "order-1",
|
|
"name": "order",
|
|
"value": "={{ $json.order }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "description-1",
|
|
"name": "description",
|
|
"value": "={{ $json.description }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "quantity-1",
|
|
"name": "quantity",
|
|
"value": "={{ $json.quantity }}",
|
|
"type": "number"
|
|
},
|
|
{
|
|
"id": "notes-1",
|
|
"name": "notes",
|
|
"value": "={{ $json.notes }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "33ce0e0d-b1ab-4683-9b1e-7c8322012372",
|
|
"name": "item_status",
|
|
"value": "pending",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "003e4156-642d-439f-9bc3-50e1ee50a77e",
|
|
"name": "selling_price",
|
|
"value": "={{ $json.selling_price }}",
|
|
"type": "number"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "1fd4e23b-82f4-49bd-a0a5-31a0cd490d6d",
|
|
"name": "Create order_items1 (Multi)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3248,
|
|
288
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"binaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"id": "74379390-d7d0-4303-863b-1876ff4f8c3b",
|
|
"name": "Update orders (Multi)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3696,
|
|
960
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"mode": "chooseBranch",
|
|
"useDataOfInput": "=1"
|
|
},
|
|
"id": "bfb6f54a-0156-481a-a26b-7bb04abb434a",
|
|
"name": "Merge order completion",
|
|
"type": "n8n-nodes-base.merge",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
3472,
|
|
336
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "8ff90d7c-7f64-4b83-ad10-e5f5c14dce31",
|
|
"name": "contact_name",
|
|
"value": "={{ $json.from_name || 'Unknown - needs review' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "9ad419b0-56e5-465b-b234-e2a688fd1c2b",
|
|
"name": "email_sender",
|
|
"value": "={{ $json.from_email }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "a7aa7543-62dc-4a3e-a2f0-8b0c37b55def",
|
|
"name": "customer",
|
|
"value": "xmd68ps32k1x5fu",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "fdb069b1-62ce-4383-b058-9efac4757386",
|
|
"name": "email_body",
|
|
"value": "={{ ($json.body_text || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "2b371925-94d9-4bf2-a069-0f5eef6b644a",
|
|
"name": "email_message_id",
|
|
"value": "={{ $json.message_id }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "3bdda609-6421-4076-9c26-ed2119fba09e",
|
|
"name": "email_subject",
|
|
"value": "={{ $json.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "15f1fa2e-6cae-457b-b712-a7e851cd3293",
|
|
"name": "order_number",
|
|
"value": "NO-REF",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "c438ce77-4241-459e-ac9e-6a341a4e23f8",
|
|
"name": "company",
|
|
"value": "fufdg0k9q128wzv",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "344320aa-d757-49f1-8b85-79345ed0627d",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "f09a7a75-7782-4d21-bc49-aca455ed41ca",
|
|
"name": "description",
|
|
"value": "=[NEEDS REVIEW] AUTO-FLAGGED: multi-order routing fired but no PO number could be found in the subject/body. Raw email body is stored in email_body \u2014 needs manual entry.",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "62ca378a-d7c7-4a94-87ee-88a846dd556a",
|
|
"name": "Create order (no PO detected - raw)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-432,
|
|
1984
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"formBinaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"id": "94229c67-a226-4fce-9fe8-3cebc4f5563f",
|
|
"name": "Update order (attach raw file - whole email)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
240,
|
|
1984
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "cbb4454a-969d-43c1-86ca-65f4088a260f",
|
|
"name": "contact_name",
|
|
"value": "={{ $('Limit (Multi)').item.json.envelope.sender[0].name || 'Unknown - needs review' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "fb260728-17a0-4812-9773-a205cabbbfec",
|
|
"name": "email_sender",
|
|
"value": "={{ $('Limit (Multi)').item.json.envelope.sender[0].address }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "d55bace2-1c98-47e8-a951-633e9c0b2b86",
|
|
"name": "customer",
|
|
"value": "xmd68ps32k1x5fu",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "5e16996d-334d-4765-b592-2cb863afcb83",
|
|
"name": "email_body",
|
|
"value": "={{ ($('Limit (Multi)').item.json.textContent || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "66ce98be-15fb-4c4c-9780-878a0976324d",
|
|
"name": "email_message_id",
|
|
"value": "={{ $('Limit (Multi)').item.json.headers['message-id'] }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "69b8c4a9-361f-4e74-91c4-74ed962a67de",
|
|
"name": "email_subject",
|
|
"value": "={{ $('Limit (Multi)').item.json.envelope.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "6b4c244c-859e-4c7b-984e-410cf9b8a8ce",
|
|
"name": "order_number",
|
|
"value": "={{ $('Limit (Multi)').item.json.po_number || 'NO-REF' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "01a2b13f-07af-4352-9eea-6e23f5601455",
|
|
"name": "company",
|
|
"value": "fufdg0k9q128wzv",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "a0ad6940-50b9-45e3-b264-a04da3d81ecd",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "b079044e-ab28-46d1-9eda-696708200eeb",
|
|
"name": "description",
|
|
"value": "=[NEEDS REVIEW] AUTO-FLAGGED: AI Agent could not extract any line items for this PO. Raw extracted text: {{ ($('Code in JavaScript4 (Multi)').item.json.aggregatedText || '').substring(0, 2000) }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "2a203a70-6b63-4a9e-bcbf-58675401d5a5",
|
|
"name": "Create order (no items extracted - raw)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2576,
|
|
1152
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"formBinaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] }}",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"id": "254a4383-97fc-43b2-aae0-45b5bccef54d",
|
|
"name": "Update order (attach raw file - per order)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3248,
|
|
1152
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "moveEmail",
|
|
"sourceMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $('CHECK FOR NEW MAIL').item.json.uid }}",
|
|
"destinationMailbox": {
|
|
"__rl": true,
|
|
"value": "INBOX.202608.Completed",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX.202608.Completed"
|
|
}
|
|
},
|
|
"id": "93f53e58-b89d-49a1-8826-d718103dfccd",
|
|
"name": "MoveEmail email2 (Multi)",
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
240,
|
|
1360
|
|
],
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "create",
|
|
"parameters": {
|
|
"allElements": true,
|
|
"relation": [
|
|
"customer"
|
|
],
|
|
"fields": [
|
|
"id"
|
|
]
|
|
},
|
|
"fields": {
|
|
"assignments": [
|
|
{
|
|
"id": "127fa8c0-f3e8-4267-84da-99d5cf816713",
|
|
"name": "contact_name",
|
|
"value": "={{ $('Limit').item.json.envelope?.sender?.[0]?.name || 'Unknown - needs review' }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "1e4e6ac1-80dc-4960-bc7d-f42195b3b46a",
|
|
"name": "email_sender",
|
|
"value": "={{ $('Limit').item.json.envelope?.sender?.[0]?.address }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "4446dc18-a58a-47be-aff3-348aef3f0a5b",
|
|
"name": "customer",
|
|
"value": "xmd68ps32k1x5fu",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "2cd882aa-d188-4d17-9c73-bf2d46bfe45b",
|
|
"name": "email_body",
|
|
"value": "={{ ($('Limit').item.json.textContent || '').substring(0, 4000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "047b10fe-9229-48c0-95c2-354405c74025",
|
|
"name": "email_message_id",
|
|
"value": "={{ $('Limit').item.json.headers?.['message-id'] }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "9b2bb6ca-91e8-45dd-9f48-d476080b383e",
|
|
"name": "email_subject",
|
|
"value": "={{ $('Limit').item.json.envelope?.subject || $('Limit').item.json.subject }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "386cbdfa-e906-44e4-ac13-8530dae92a71",
|
|
"name": "order_number",
|
|
"value": "NO-REF",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "c7f6408c-aec8-4055-bf70-66bbc13cecbd",
|
|
"name": "company",
|
|
"value": "fufdg0k9q128wzv",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "13892b22-b4ca-4447-bb25-a945aef8e18c",
|
|
"name": "status",
|
|
"value": "new",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "1d6eb541-bd2f-4943-85de-2374c15212e7",
|
|
"name": "description",
|
|
"value": "=[NEEDS REVIEW] AUTO-FLAGGED: AI Agent could not extract any line items from this order email. Raw extracted text: {{ ($('Code in JavaScript4').item.json.aggregatedText || '').substring(0, 2000) }}",
|
|
"type": "string"
|
|
},
|
|
{
|
|
"id": "e415897a-8942-4737-8bec-f48e19a106d0",
|
|
"name": "rep",
|
|
"value": "={{ $('Limit').item.json.rep }}",
|
|
"type": "string"
|
|
}
|
|
]
|
|
},
|
|
"requestOptions": {}
|
|
},
|
|
"id": "97cf61bd-97ee-4c3b-a41b-ffc4d50f996b",
|
|
"name": "Create order (single - no items - raw)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2800,
|
|
2624
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "orders",
|
|
"operation": "update",
|
|
"elementId": "={{ $json.orderId }}",
|
|
"parameters": {},
|
|
"bodyType": [
|
|
"formBinaryData"
|
|
],
|
|
"binaryPropertyName": "={{ Object.keys($binary)[0] ?? '' }} ",
|
|
"binaryFieldName": "order_attachements+",
|
|
"requestOptions": {}
|
|
},
|
|
"id": "bfd49b45-1d0e-4750-a775-bb3dbf24a0a8",
|
|
"name": "Update order (attach raw file - single)",
|
|
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3840,
|
|
2480
|
|
],
|
|
"credentials": {
|
|
"pocketbaseHttpApi": {
|
|
"id": "hLwB1QPIJEWsuVyW",
|
|
"name": "PocketBase HTTP account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create orders').first().json.id;\n\n// Use only what was actually passed to this node (not the full Split Attachments1 output)\n// so downstream SplitInBatches loops don't re-inject filtered-out items.\nconst attachmentItems = $input.all().filter(i => {\n if (!i.binary || Object.keys(i.binary).length === 0) return false;\n const bKeys = Object.keys(i.binary);\n return bKeys.some(k => i.binary[k].fileName && i.binary[k].fileName !== 'None' && i.binary[k].fileName !== 'null');\n});\n\nif (attachmentItems.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn attachmentItems.map(i => {\n const cleanBinary = {};\n for (const [k, v] of Object.entries(i.binary)) {\n if (!v.fileName || v.fileName === 'None' || v.fileName === 'null') continue;\n cleanBinary[k] = v;\n }\n if (Object.keys(cleanBinary).length === 0) return null;\n return {\n json: { orderId, noAttachments: false },\n binary: cleanBinary,\n };\n}).filter(Boolean);"
|
|
},
|
|
"id": "0102cef1-35cc-4496-b844-56c13b3088fa",
|
|
"name": "Prepare attachment list",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3248,
|
|
1648
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "b4ce7c83-a9f2-4d5d-b6f9-92d572b6c2f1",
|
|
"name": "Attachment loop",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
3472,
|
|
1648
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"combinator": "and",
|
|
"conditions": [
|
|
{
|
|
"id": "d7345329-0208-4a34-ae62-eaab8f0ea5c8",
|
|
"leftValue": "={{ $json.noAttachments }}",
|
|
"operator": {
|
|
"operation": "notEquals",
|
|
"type": "boolean"
|
|
},
|
|
"rightValue": true
|
|
},
|
|
{
|
|
"id": "a9c12345-file-name-valid",
|
|
"leftValue": "={{ Object.values($binary || {}).length > 0 && Object.values($binary)[0].fileName != null && Object.values($binary)[0].fileName !== 'None' && Object.values($binary)[0].fileName !== 'null' }}",
|
|
"operator": {
|
|
"operation": "equals",
|
|
"type": "boolean"
|
|
},
|
|
"rightValue": true
|
|
}
|
|
],
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
}
|
|
}
|
|
},
|
|
"id": "88a759c4-8c13-43c7-b868-d243c166b836",
|
|
"name": "Has attachment to upload?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
3696,
|
|
1232
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create orders (Multi)').first().json.id;\nconst attachmentItems = $('Split Attachments1 (Multi)').all().filter(i => i.binary && Object.keys(i.binary).length > 0);\n\nif (attachmentItems.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn attachmentItems.map(i => ({\n json: { orderId, noAttachments: false },\n binary: i.binary,\n}));"
|
|
},
|
|
"id": "434ba73b-067c-4e1d-a564-e77779a16aca",
|
|
"name": "Prepare attachment list (Multi)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3024,
|
|
960
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "211984cb-52b7-47c5-aa76-09c6bdc12aa7",
|
|
"name": "Attachment loop (Multi)",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
3248,
|
|
960
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "7ba3601c-53d0-4117-a891-b54d639dc346",
|
|
"leftValue": "={{ $json.noAttachments }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "notEquals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "fafe7217-f34e-4cae-a87a-9eea01fcab81",
|
|
"name": "Has attachment to upload? (Multi)",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
3472,
|
|
896
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create order (no PO detected - raw)').first().json.id;\nconst binary = $input.first().binary || {};\nconst keys = Object.keys(binary);\n\nif (keys.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn keys.map(k => ({\n json: { orderId, noAttachments: false },\n binary: { [k]: binary[k] },\n}));"
|
|
},
|
|
"id": "ab4444a7-5f75-4177-a342-740a2478397b",
|
|
"name": "Prepare attachment list (whole email)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-208,
|
|
1984
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "bf6b5c3f-a8b9-49c7-aaa8-56d2d90f31e9",
|
|
"name": "Attachment loop (whole email)",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
16,
|
|
1984
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create order (no items extracted - raw)').first().json.id;\nconst attachmentItems = $('Split Attachments1 (Multi)').all().filter(i => i.binary && Object.keys(i.binary).length > 0);\n\nif (attachmentItems.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn attachmentItems.map(i => ({\n json: { orderId, noAttachments: false },\n binary: i.binary,\n}));"
|
|
},
|
|
"id": "b909e644-97be-4bb7-ae8a-a7e4bb1c1c83",
|
|
"name": "Prepare attachment list (per order)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2800,
|
|
1152
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "58907510-2052-408f-85fe-2811aff80f32",
|
|
"name": "Attachment loop (per order)",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
3024,
|
|
1152
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const orderId = $('Create order (single - no items - raw)').first().json.id;\n\nconst attachmentItems = $input.all().filter(i => {\n if (!i.binary || Object.keys(i.binary).length === 0) return false;\n const bKeys = Object.keys(i.binary);\n return bKeys.some(k => i.binary[k].fileName && i.binary[k].fileName !== 'None' && i.binary[k].fileName !== 'null');\n});\n\nif (attachmentItems.length === 0) {\n return [{ json: { orderId, noAttachments: true } }];\n}\n\nreturn attachmentItems.map(i => {\n const cleanBinary = {};\n for (const [k, v] of Object.entries(i.binary)) {\n if (!v.fileName || v.fileName === 'None' || v.fileName === 'null') continue;\n cleanBinary[k] = v;\n }\n if (Object.keys(cleanBinary).length === 0) return null;\n return {\n json: { orderId, noAttachments: false },\n binary: cleanBinary,\n };\n}).filter(Boolean);"
|
|
},
|
|
"id": "1b210b9b-465b-4836-bd84-a05a479adf7f",
|
|
"name": "Prepare attachment list (single fallback)",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3024,
|
|
2624
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "fbd21c7d-4c30-4b36-b53d-a0bff29eb082",
|
|
"name": "Attachment loop (single fallback)",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
3248,
|
|
2576
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"model": "MiniMax-M2.5",
|
|
"options": {}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatMinimax",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1728,
|
|
1184
|
|
],
|
|
"id": "d3cb0978-d756-41e3-8d25-5b1f5e215b26",
|
|
"name": "MiniMax Chat Model2",
|
|
"credentials": {
|
|
"minimaxApi": {
|
|
"id": "kn97KHCrkyTm3v2g",
|
|
"name": "MiniMax account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "loose",
|
|
"version": 3
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "a1b2c3d4-has-binary",
|
|
"leftValue": "={{ $json.noAttachments }}",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
},
|
|
"rightValue": "false"
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"looseTypeValidation": true,
|
|
"options": {}
|
|
},
|
|
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
"name": "Has binary? (single fallback)",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2.3,
|
|
"position": [
|
|
3504,
|
|
2496
|
|
]
|
|
},
|
|
{
|
|
"id": "f5d15e31-5ee7-49d4-a4e3-2a21e15bf2bf",
|
|
"name": "Sanitise binary for upload",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3808,
|
|
1328
|
|
],
|
|
"parameters": {
|
|
"jsCode": "// Last gate before PocketBase upload:\n// Strip any binary entry that has no fileName (inline email images)\n// If nothing valid remains, skip this item entirely.\nconst item = $input.item;\nconst cleanBinary = {};\n\nfor (const [k, v] of Object.entries(item.binary || {})) {\n const name = v.fileName;\n if (name && name !== 'None' && name !== 'null' && name.trim() !== '') {\n cleanBinary[k] = v;\n }\n}\n\n// No valid files on this item \u2014 drop it silently\nif (Object.keys(cleanBinary).length === 0) {\n return [];\n}\n\nreturn [{\n json: item.json,\n binary: cleanBinary,\n}];"
|
|
}
|
|
},
|
|
{
|
|
"id": "282149bd-070a-41dd-9c4b-468f1d0b6aa1",
|
|
"name": "SetEmailFlags email (mark on fetch)",
|
|
"type": "n8n-nodes-imap-enhanced.imapEnhanced",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-1700,
|
|
1456
|
|
],
|
|
"parameters": {
|
|
"resource": "email",
|
|
"operation": "setEmailFlags",
|
|
"mailboxPath": {
|
|
"__rl": true,
|
|
"value": "INBOX",
|
|
"mode": "list",
|
|
"cachedResultName": "INBOX"
|
|
},
|
|
"emailUid": "={{ $('CHECK FOR NEW MAIL').item.json.uid }}",
|
|
"flags": {
|
|
"\\Seen": true
|
|
}
|
|
},
|
|
"credentials": {
|
|
"imapApi": {
|
|
"id": "vPyqCkQOwcnLgJxZ",
|
|
"name": "ORDERS @ WFSALES"
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"connections": {
|
|
"Analyze image1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript3",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Switch1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Extract PDF",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from CSV1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from XLS1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from XLSX1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from TXT1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 2
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Analyze image1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Analyze image1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract PDF",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript3": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 6
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from CSV1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 2
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from TXT1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 5
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from XLSX1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 4
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from XLS1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 3
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create order_items1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Merge1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "PocketBase1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Search customers": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create orders",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Minimax Chat Model4": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "Structured Output Parser",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Structured Output Parser": {
|
|
"ai_outputParser": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "ai_outputParser",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"AI Agent": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Minimax Chat Model": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract PDF": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If3",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Split Attachments1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Switch1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Merge2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"PocketBase1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Limit": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Download ATTACHMENT",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Search customers",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Create order (single - no items - raw)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Workflow",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "MoveEmail email2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "MoveEmail email2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"MESSAGE FOR AI": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Aggregate": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MESSAGE FOR AI",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"MiniMax Chat Model": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Structured Output Parser1": {
|
|
"ai_outputParser": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_outputParser",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"AI Agent1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Ai Resposes",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"MiniMax Chat Model1": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "Structured Output Parser1",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Ai Resposes": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Switch",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Switch": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "UPDATE AI ORDER",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "UPDATE AI ORDER ITEM",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_suppliers": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_price_list": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_order_items": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_stock": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_orders": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_quote_items": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"pb_quotes": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Ollama Chat Model": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent1",
|
|
"type": "ai_languageModel",
|
|
"index": 1
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"CHECK FOR NEW MAIL": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "SetEmailFlags email (mark on fetch)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Schedule Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "CHECK FOR NEW MAIL",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order_items1": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Aggregate",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"UPDATE AI ORDER": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"UPDATE AI ORDER ITEM": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create orders": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript2",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Merge1",
|
|
"type": "main",
|
|
"index": 1
|
|
},
|
|
{
|
|
"node": "Prepare attachment list",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Download ATTACHMENT": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Split Attachments1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"PDF to PNG": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Analyze image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If3": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 1
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "PDF to PNG",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Analyze image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update orders": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript4": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"MoveEmail email2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "SetEmailFlags email",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Send an Email": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Classify Email (Deterministic)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Email",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Email": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send an Email",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Download ATTACHMENT (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "AI Classify Email (cheap)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Limit",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Cheap Classifier Model (Minimax)": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Classify Email (cheap)",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"AI Classify Email (cheap)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse AI Classification",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse AI Classification": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Email (AI Result)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Email (AI Result)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send an Email",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Move to SUPPLIER CORRESPONDENCE1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Limit",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Download ATTACHMENT (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Group attachments by PO",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Group attachments by PO": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Has valid orders?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Has valid orders?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Per-order loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Create order (no PO detected - raw)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Per-order loop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email2 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Limit (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Limit (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Split Attachments1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Split Attachments1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Switch1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Switch1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Extract PDF (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from CSV1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from XLS1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from XLSX1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract from TXT1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Analyze image1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Analyze image1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Extract PDF (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract PDF (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If3 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If3 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "PDF to PNG (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"PDF to PNG (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Analyze image (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Analyze image (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from CSV1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from XLS1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from XLSX1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Extract from TXT1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Analyze image1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript3 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript3 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript4 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript4 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "AI Agent (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Minimax Chat Model (Multi)": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent (Multi)",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Structured Output Parser (Multi)": {
|
|
"ai_outputParser": [
|
|
[
|
|
{
|
|
"node": "AI Agent (Multi)",
|
|
"type": "ai_outputParser",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"AI Agent (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "If1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"If1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Search customers (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Create order (no items extracted - raw)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Search customers (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create orders (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create orders (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Code in JavaScript2 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
},
|
|
{
|
|
"node": "Prepare attachment list (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Code in JavaScript2 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create order_items1 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order_items1 (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge order completion",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update orders (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Merge order completion": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Per-order loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order (no PO detected - raw)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare attachment list (whole email)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update order (attach raw file - whole email)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (whole email)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order (no items extracted - raw)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare attachment list (per order)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update order (attach raw file - per order)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (per order)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create order (single - no items - raw)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare attachment list (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update order (attach raw file - single)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Has attachment to upload?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Has attachment to upload?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Sanitise binary for upload",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Attachment loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Merge order completion",
|
|
"type": "main",
|
|
"index": 1
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Has attachment to upload? (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Has attachment to upload? (Multi)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Update orders (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Attachment loop (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list (whole email)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (whole email)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop (whole email)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email2 (Multi)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Update order (attach raw file - whole email)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list (per order)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (per order)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop (per order)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Per-order loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Update order (attach raw file - per order)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare attachment list (single fallback)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Attachment loop (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Attachment loop (single fallback)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "MoveEmail email1",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Has binary? (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"MiniMax Chat Model2": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "Structured Output Parser (Multi)",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Has binary? (single fallback)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Update order (attach raw file - single)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Attachment loop (single fallback)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Sanitise binary for upload": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Update orders",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"SetEmailFlags email (mark on fetch)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Classify Email (Deterministic)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"nodeGroups": [],
|
|
"authors": "David Els (via MCP)",
|
|
"name": "Fix: mark email as Seen immediately after IMAP fetch",
|
|
"description": "Prevents the error-loop duplicate-creation cycle. Previously, if any downstream step (Classify, Route, Download ATTACHMENT, PB write) failed or the workflow was interrupted, the email stayed in INBOX with the Seen flag unset. The next scheduled poll re-fetched the same email, leading to another failed run and another duplicate order. With this change, the email is marked Seen immediately after CHECK FOR NEW MAIL returns it, so a downstream failure no longer leaves the email unread for the next poll. The existing MoveEmail email2 + SetEmailFlags email chain (which moves to INBOX.202608.Completed after PB success) is unchanged \u2014 this just adds a belt to the suspenders.",
|
|
"autosaved": false,
|
|
"workflowPublishHistory": [
|
|
{
|
|
"createdAt": "2026-08-18T21:57:23.866Z",
|
|
"id": 747,
|
|
"workflowId": "DWkxko0SS1uoelsg",
|
|
"versionId": "c7ffa24e-cc0f-4863-b6ae-cdaf6a8c4b41",
|
|
"event": "activated",
|
|
"userId": "4f770df2-bd2f-4436-b153-b27004efc7b6"
|
|
}
|
|
]
|
|
}
|
|
} |