WFDASH-WEBAPP/n8n_workfows/SNAPSHOT-WFDASH-v8-from-live-2026-08-21/DWkxko0SS1uoelsg.SIMPLIFIED-2026-08-24.json

1 line
No EOL
255 KiB
JSON

{"updatedAt":"2026-08-24T15:03:03.368Z","createdAt":"2026-08-15T22:15:05.317Z","id":"DWkxko0SS1uoelsg","name":"WFDASH — ORDER MONITOR (Unified: Single + Multi-Order)","description":"WFDASH — 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,3520],"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,3520],"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,2448],"id":"c7ae6cf1-0d69-4405-a9f0-99d84c6a7d3f","name":"Extract from CSV1"},{"parameters":{"operation":"text","options":{}},"type":"n8n-nodes-base.extractFromFile","typeVersion":1.1,"position":[1136,3328],"id":"4766ba0f-b5e5-434d-af89-c0acf6e0eb56","name":"Extract from TXT1"},{"parameters":{"operation":"xlsx","options":{}},"type":"n8n-nodes-base.extractFromFile","typeVersion":1.1,"position":[1136,3136],"id":"4a6f899a-3636-4068-8c55-da6d74c01ec3","name":"Extract from XLSX1"},{"parameters":{"operation":"xls","options":{}},"type":"n8n-nodes-base.extractFromFile","typeVersion":1.1,"position":[1136,2640],"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,2816],"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":[4144,2624],"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":[4368,2624],"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":[4592,1952],"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":[4816,1952],"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":[3696,2272],"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":[2520,3624],"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":[2440,3416]},{"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 — 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 — 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 — 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: \"\"` — 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":[2336,3200],"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":[2312,3416],"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,2224],"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 — 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,2928]},{"parameters":{"numberInputs":7},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[1360,3120],"id":"8e4b7968-edb8-4ad2-9370-bbf860d64cea","name":"Merge2"},{"parameters":{},"type":"n8n-nodes-base.limit","typeVersion":1,"position":[-432,2928],"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":[3024,3200],"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.Completed","mode":"list","cachedResultName":"INBOX.202608.Completed"}},"type":"n8n-nodes-imap-enhanced.imapEnhanced","typeVersion":1,"position":[3904,3360],"id":"ce07cbfd-5159-4333-98a9-f65d13969eba","name":"MoveEmail email1","credentials":{"imapApi":{"id":"vPyqCkQOwcnLgJxZ","name":"ORDERS @ WFSALES"}},"notes":"DESTINATION CHANGED 2026-08-24: was INBOX.202608.Failed. Now sends to Completed so even if the LLM extraction produced no items, the operator sees the email as 'processed' and the order lands in /orders for manual fill-in. Real errors (PB/network) still trigger the workflow-level error handler (kVylcetxy858PcfP) which sends a Telegram alert."},{"parameters":{"workflowId":{"__rl":true,"value":"Fm1c24RCfEQt9Qkn","mode":"list","cachedResultUrl":"/workflow/Fm1c24RCfEQt9Qkn","cachedResultName":"WFDASH — 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":[7456,3024],"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":[7232,2624],"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,1744],"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] } ← items\n// - Upstream \"Create orders\" output: full parent order\n//\n// Output: { userMessage, target, parentId, items }\n// - userMessage: the assembled prompt string (→ AI Agent)\n// - target: \"order\"\n// - parentId: parent order id (→ PATCH-back to set ai_insights)\n// - items: line item ids (→ 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 — 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 — 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// — 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// ── Fetch sprint categories from PocketBase ─────────────────────────────\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 — the workflow still produces a useful userMessage\n categoriesText = '(category fetch failed — 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":[4816,2624],"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":[4592,2624],"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: {...} } — 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":[6560,2624],"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 — historical quotes\n- pb_quote_items — historical quote line items (filter by quote id, sku, or description)\n- pb_orders — historical orders\n- pb_order_items — historical order line items (filter by order id, sku, or description)\n- pb_stock — current stock for a SKU or by description\n- pb_price_list — 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` — these are the only valid `ref_id`\nvalues. Never invent ids.\n\n# Matching items without a SKU — 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 — Build keywords\n\nTake the description, drop noise:\n \"O19 - PPE NOTICE BOARD SIGN 300 X 300 PVC\" →\n \"ppe notice board sign pvc 300\"\n\n## Step 2 — 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 — 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 — 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 — 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 — 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 — it's already in the message.\nYou DO need to fetch historical data via tools.\n\n# What to look for — DOCUMENT level (the parent)\n\nUse the tools to find:\n\n1. Duplicates / near-duplicates — 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 — 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 — 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 — 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 — 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 — 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 → 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 — WRONG (raw ID):\n { \"supplier_id\": \"rlhxhaw73dxmsu3\", \"supplier_name\": \"rlhxhaw73dxmsu3\" }\n\nExample — 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 — 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 — 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 — 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 — what you found, what's missing, anything noteworthy>\"\n }\n ]\n}\n\n# Per-item fields — 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 — only include it if you have one.\n\nDon't try to fill these for items that genuinely have no data —\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` — 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 — 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 — 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 — 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":[5528,2624],"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":[5048,2840],"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":[6200,2840],"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":[6280,3048],"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":[7008,2320],"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":[7008,2720],"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":[6784,2624],"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":[5304,2840],"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":[5432,2840],"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":[5560,2840],"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":[5688,2840],"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":[5816,2840],"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":[5944,2840],"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":[6072,2840],"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":[5176,2840],"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,1744],"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":[3920,2272],"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,2928],"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,2160],"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,2224],"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,2160],"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"],"requestOptions":{}},"type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[5696,2224],"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":[1584,3200],"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":[7456,2496],"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":[7680,2496],"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,576],"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,432],"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":"// ─────────────────────────────────────────────────────────────\n// EDIT: add supplier domains to this list, or swap for a live\n// pb_suppliers lookup later\n// ─────────────────────────────────────────────────────────────\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 // ── 1. CONFIRMED BOUNCE — RFC 3464 delivery-status report ──\n const isConfirmedBounce = contentType.includes('report-type=delivery-status');\n\n // ── 2. CONFIRMED SUPPLIER — domain match ───────────────────\n const senderDomain = (sender.split('@')[1] || '');\n const isConfirmedSupplier = !isConfirmedBounce &&\n supplierDomains.some(d => senderDomain.endsWith(d));\n\n // ── 3. MULTI-ORDER — multiple PDFs OR multiple PO numbers ──\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 // ── 4. WEAK SIGNALS — hand off to AI ──────────────────────\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 // ── ROUTE ──────────────────────────────────────────────────\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,1744]},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"id":"r-bounce","leftValue":"={{ $json.route }}","rightValue":"bounce","operator":{"type":"string","operation":"equals"}}],"combinator":"and"}},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"id":"r-supplier","leftValue":"={{ $json.route }}","rightValue":"supplier","operator":{"type":"string","operation":"equals"}}],"combinator":"and"}},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"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":1},"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,1696]},{"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":[-1160,2616],"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 — not a real message from a person.\nSUPPLIER = a message from one of our own suppliers (order confirmations, invoices, stock/backorder updates) — 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,2384]},{"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,2384]},{"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,2368]},{"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,1568],"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,1568]},{"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,1568]},{"parameters":{"options":{}},"id":"dbfb84ab-b929-486c-9487-3aec267e78b0","name":"Per-order loop","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[-432,1456]},{"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,768]},{"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,768]},{"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,656]},{"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,1008]},{"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,640]},{"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 — 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 — 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: \"\"` — 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":[1840,640]},{"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":[1816,872],"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":[1944,872]},{"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":[2800,640]},{"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":[3472,432],"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":[3696,432],"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":[3920,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":[4144,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":[4592,1056],"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":[4368,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":"={{ $json.subject || $json.email_subject || '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] AI extraction could not parse line items. Raw email + attachments attached below.\n","type":"string"}]},"requestOptions":{}},"id":"62ca378a-d7c7-4a94-87ee-88a846dd556a","name":"Create order (no PO detected - raw)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3024,1904],"credentials":{"pocketbaseHttpApi":{"id":"hLwB1QPIJEWsuVyW","name":"PocketBase HTTP account"}}},{"parameters":{"resource":"orders","operation":"update","elementId":"={{ $json.orderId }}","parameters":{},"bodyType":["formBinaryData"],"requestOptions":{}},"id":"94229c67-a226-4fce-9fe8-3cebc4f5563f","name":"Update order (attach raw file - whole email)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3696,1952],"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":[3024,1184],"credentials":{"pocketbaseHttpApi":{"id":"hLwB1QPIJEWsuVyW","name":"PocketBase HTTP account"}}},{"parameters":{"resource":"orders","operation":"update","elementId":"={{ $json.orderId }}","parameters":{},"bodyType":["formBinaryData"],"requestOptions":{}},"id":"254a4383-97fc-43b2-aae0-45b5bccef54d","name":"Update order (attach raw file - per order)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3696,1456],"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":[3696,1264],"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":"={{ $('Limit').item.json.envelope?.subject || $('Limit').item.json.subject || '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] AI extraction could not parse line items. Raw email + attachments attached below.\n","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":[3248,3376],"credentials":{"pocketbaseHttpApi":{"id":"hLwB1QPIJEWsuVyW","name":"PocketBase HTTP account"}}},{"parameters":{"resource":"orders","operation":"update","elementId":"={{ $json.orderId }}","parameters":{},"bodyType":["formBinaryData"],"requestOptions":{}},"id":"bfd49b45-1d0e-4750-a775-bb3dbf24a0a8","name":"Update order (attach raw file - single)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[4144,3280],"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":[4144,2272]},{"parameters":{"options":{}},"id":"b4ce7c83-a9f2-4d5d-b6f9-92d572b6c2f1","name":"Attachment loop","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[4368,2272]},{"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}},"options":{}},"id":"88a759c4-8c13-43c7-b868-d243c166b836","name":"Has attachment to upload?","type":"n8n-nodes-base.if","typeVersion":2.3,"position":[4592,2144]},{"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":[3920,992]},{"parameters":{"options":{}},"id":"211984cb-52b7-47c5-aa76-09c6bdc12aa7","name":"Attachment loop (Multi)","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[4144,992]},{"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":[4368,992]},{"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":[3248,1904]},{"parameters":{"options":{}},"id":"bf6b5c3f-a8b9-49c7-aaa8-56d2d90f31e9","name":"Attachment loop (whole email)","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[3472,1904]},{"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":[3248,1184]},{"parameters":{"options":{}},"id":"58907510-2052-408f-85fe-2811aff80f32","name":"Attachment loop (per order)","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[3472,1184]},{"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":[3472,3376]},{"parameters":{"options":{}},"id":"fbd21c7d-4c30-4b36-b53d-a0bff29eb082","name":"Attachment loop (single fallback)","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[3696,3376]},{"parameters":{"model":"MiniMax-M2.5","options":{}},"type":"@n8n/n8n-nodes-langchain.lmChatMinimax","typeVersion":1,"position":[2024,1080],"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":[3920,3120]},{"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 — drop it silently\nif (Object.keys(cleanBinary).length === 0) {\n return [];\n}\n\nreturn [{\n json: item.json,\n binary: cleanBinary,\n}];"},"id":"f5d15e31-5ee7-49d4-a4e3-2a21e15bf2bf","name":"Sanitise binary for upload","type":"n8n-nodes-base.code","typeVersion":2,"position":[4816,2144]},{"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}},"id":"282149bd-070a-41dd-9c4b-468f1d0b6aa1","name":"SetEmailFlags email (mark on fetch)","type":"n8n-nodes-imap-enhanced.imapEnhanced","typeVersion":1,"position":[-2128,3744],"credentials":{"imapApi":{"id":"vPyqCkQOwcnLgJxZ","name":"ORDERS @ WFSALES"}},"disabled":true,"notes":"DISABLED 2026-08-24 — this node was stripping the email body (envelope/textContent/headers), causing the AI Agent downstream to receive empty prompts and If1 to route every email to MoveEmail email1 (INBOX.202608.Failed). Removed from chain. MoveEmail on success path handles the Seen flag implicitly via mailbox move; on failure the email stays Unseen and gets re-polled next cycle (free retry). See docs/audit/order-pipeline-implementation-2026-08-21.md for context."},{"parameters":{"resource":"orders","operation":"getList","parameters":{},"requestOptions":{}},"id":"0c8d53f6-057e-4dc2-b0ad-7add37ae26d3","name":"Search orders by email_message_id","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3248,2704],"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":[3472,2704]},{"parameters":{"jsCode":"// Branch hit when 'Search orders by email_message_id' returns ≥1 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 §2.\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":[7232,2976]},{"parameters":{"resource":"orders","operation":"getList","parameters":{},"requestOptions":{}},"id":"e9507973-bc42-4321-83e5-db03870ab9bd","name":"Search orders by email_message_id (Multi)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3024,528],"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":[3248,528]},{"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":[3472,992]},{"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 — 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 §3.\n//\n// ──────────────────────────────────────────────────────────────────\n// 1. Email signature parse\n// ──────────────────────────────────────────────────────────────────\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// ──────────────────────────────────────────────────────────────────\n// 2. PDF \"Requester\" / \"Deliver To\" / \"Attention\" parse\n// ──────────────────────────────────────────────────────────────────\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 — fall back to email sig.\n}\n\n// ──────────────────────────────────────────────────────────────────\n// 3. Pick the best candidate (PDF > email signature > nothing)\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":"820c8fc4-4853-45b7-a0ae-1d934e21eddf","name":"Extract contact name (deterministic)","type":"n8n-nodes-base.code","typeVersion":2,"position":[1952,3200]},{"parameters":{"jsCode":"// Deterministic contact_name extraction (multi-branch).\n// Same algorithm as the single-branch 'Extract contact name\n// (deterministic)' — 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 §3.\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 — 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":[1584,640]},{"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 §3.\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 — 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 — 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":[2800,3200]},{"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":[2448,640]}],"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":"Classify Email (Deterministic)","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":[]},"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":"57bf8616-0a62-4b7a-96c1-18ede2e55e0b","activeVersionId":"57bf8616-0a62-4b7a-96c1-18ede2e55e0b","versionCounter":66,"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-24T15:03:03.375Z","createdAt":"2026-08-24T15:03:03.375Z","versionId":"57bf8616-0a62-4b7a-96c1-18ede2e55e0b","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,3520],"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,3520],"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,2448],"id":"c7ae6cf1-0d69-4405-a9f0-99d84c6a7d3f","name":"Extract from CSV1"},{"parameters":{"operation":"text","options":{}},"type":"n8n-nodes-base.extractFromFile","typeVersion":1.1,"position":[1136,3328],"id":"4766ba0f-b5e5-434d-af89-c0acf6e0eb56","name":"Extract from TXT1"},{"parameters":{"operation":"xlsx","options":{}},"type":"n8n-nodes-base.extractFromFile","typeVersion":1.1,"position":[1136,3136],"id":"4a6f899a-3636-4068-8c55-da6d74c01ec3","name":"Extract from XLSX1"},{"parameters":{"operation":"xls","options":{}},"type":"n8n-nodes-base.extractFromFile","typeVersion":1.1,"position":[1136,2640],"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,2816],"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":[4144,2624],"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":[4368,2624],"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":[4592,1952],"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":[4816,1952],"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":[3696,2272],"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":[2520,3624],"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":[2440,3416]},{"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 — 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 — 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 — 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: \"\"` — 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":[2336,3200],"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":[2312,3416],"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,2224],"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 — 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,2928]},{"parameters":{"numberInputs":7},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[1360,3120],"id":"8e4b7968-edb8-4ad2-9370-bbf860d64cea","name":"Merge2"},{"parameters":{},"type":"n8n-nodes-base.limit","typeVersion":1,"position":[-432,2928],"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":[3024,3200],"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.Completed","mode":"list","cachedResultName":"INBOX.202608.Completed"}},"type":"n8n-nodes-imap-enhanced.imapEnhanced","typeVersion":1,"position":[3904,3360],"id":"ce07cbfd-5159-4333-98a9-f65d13969eba","name":"MoveEmail email1","credentials":{"imapApi":{"id":"vPyqCkQOwcnLgJxZ","name":"ORDERS @ WFSALES"}},"notes":"DESTINATION CHANGED 2026-08-24: was INBOX.202608.Failed. Now sends to Completed so even if the LLM extraction produced no items, the operator sees the email as 'processed' and the order lands in /orders for manual fill-in. Real errors (PB/network) still trigger the workflow-level error handler (kVylcetxy858PcfP) which sends a Telegram alert."},{"parameters":{"workflowId":{"__rl":true,"value":"Fm1c24RCfEQt9Qkn","mode":"list","cachedResultUrl":"/workflow/Fm1c24RCfEQt9Qkn","cachedResultName":"WFDASH — 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":[7456,3024],"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":[7232,2624],"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,1744],"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] } ← items\n// - Upstream \"Create orders\" output: full parent order\n//\n// Output: { userMessage, target, parentId, items }\n// - userMessage: the assembled prompt string (→ AI Agent)\n// - target: \"order\"\n// - parentId: parent order id (→ PATCH-back to set ai_insights)\n// - items: line item ids (→ 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 — 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 — 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// — 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// ── Fetch sprint categories from PocketBase ─────────────────────────────\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 — the workflow still produces a useful userMessage\n categoriesText = '(category fetch failed — 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":[4816,2624],"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":[4592,2624],"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: {...} } — 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":[6560,2624],"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 — historical quotes\n- pb_quote_items — historical quote line items (filter by quote id, sku, or description)\n- pb_orders — historical orders\n- pb_order_items — historical order line items (filter by order id, sku, or description)\n- pb_stock — current stock for a SKU or by description\n- pb_price_list — 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` — these are the only valid `ref_id`\nvalues. Never invent ids.\n\n# Matching items without a SKU — 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 — Build keywords\n\nTake the description, drop noise:\n \"O19 - PPE NOTICE BOARD SIGN 300 X 300 PVC\" →\n \"ppe notice board sign pvc 300\"\n\n## Step 2 — 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 — 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 — 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 — 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 — 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 — it's already in the message.\nYou DO need to fetch historical data via tools.\n\n# What to look for — DOCUMENT level (the parent)\n\nUse the tools to find:\n\n1. Duplicates / near-duplicates — 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 — 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 — 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 — 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 — 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 — 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 → 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 — WRONG (raw ID):\n { \"supplier_id\": \"rlhxhaw73dxmsu3\", \"supplier_name\": \"rlhxhaw73dxmsu3\" }\n\nExample — 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 — 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 — 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 — 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 — what you found, what's missing, anything noteworthy>\"\n }\n ]\n}\n\n# Per-item fields — 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 — only include it if you have one.\n\nDon't try to fill these for items that genuinely have no data —\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` — 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 — 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 — 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 — 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":[5528,2624],"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":[5048,2840],"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":[6200,2840],"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":[6280,3048],"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":[7008,2320],"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":[7008,2720],"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":[6784,2624],"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":[5304,2840],"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":[5432,2840],"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":[5560,2840],"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":[5688,2840],"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":[5816,2840],"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":[5944,2840],"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":[6072,2840],"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":[5176,2840],"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,1744],"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":[3920,2272],"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,2928],"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,2160],"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,2224],"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,2160],"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"],"requestOptions":{}},"type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[5696,2224],"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":[1584,3200],"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":[7456,2496],"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":[7680,2496],"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,576],"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,432],"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":"// ─────────────────────────────────────────────────────────────\n// EDIT: add supplier domains to this list, or swap for a live\n// pb_suppliers lookup later\n// ─────────────────────────────────────────────────────────────\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 // ── 1. CONFIRMED BOUNCE — RFC 3464 delivery-status report ──\n const isConfirmedBounce = contentType.includes('report-type=delivery-status');\n\n // ── 2. CONFIRMED SUPPLIER — domain match ───────────────────\n const senderDomain = (sender.split('@')[1] || '');\n const isConfirmedSupplier = !isConfirmedBounce &&\n supplierDomains.some(d => senderDomain.endsWith(d));\n\n // ── 3. MULTI-ORDER — multiple PDFs OR multiple PO numbers ──\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 // ── 4. WEAK SIGNALS — hand off to AI ──────────────────────\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 // ── ROUTE ──────────────────────────────────────────────────\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,1744]},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"id":"r-bounce","leftValue":"={{ $json.route }}","rightValue":"bounce","operator":{"type":"string","operation":"equals"}}],"combinator":"and"}},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"id":"r-supplier","leftValue":"={{ $json.route }}","rightValue":"supplier","operator":{"type":"string","operation":"equals"}}],"combinator":"and"}},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"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":1},"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,1696]},{"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":[-1160,2616],"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 — not a real message from a person.\nSUPPLIER = a message from one of our own suppliers (order confirmations, invoices, stock/backorder updates) — 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,2384]},{"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,2384]},{"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,2368]},{"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,1568],"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,1568]},{"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,1568]},{"parameters":{"options":{}},"id":"dbfb84ab-b929-486c-9487-3aec267e78b0","name":"Per-order loop","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[-432,1456]},{"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,768]},{"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,768]},{"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,656]},{"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,1008]},{"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,640]},{"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 — 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 — 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: \"\"` — 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":[1840,640]},{"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":[1816,872],"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":[1944,872]},{"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":[2800,640]},{"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":[3472,432],"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":[3696,432],"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":[3920,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":[4144,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":[4592,1056],"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":[4368,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":"={{ $json.subject || $json.email_subject || '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] AI extraction could not parse line items. Raw email + attachments attached below.\n","type":"string"}]},"requestOptions":{}},"id":"62ca378a-d7c7-4a94-87ee-88a846dd556a","name":"Create order (no PO detected - raw)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3024,1904],"credentials":{"pocketbaseHttpApi":{"id":"hLwB1QPIJEWsuVyW","name":"PocketBase HTTP account"}}},{"parameters":{"resource":"orders","operation":"update","elementId":"={{ $json.orderId }}","parameters":{},"bodyType":["formBinaryData"],"requestOptions":{}},"id":"94229c67-a226-4fce-9fe8-3cebc4f5563f","name":"Update order (attach raw file - whole email)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3696,1952],"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":[3024,1184],"credentials":{"pocketbaseHttpApi":{"id":"hLwB1QPIJEWsuVyW","name":"PocketBase HTTP account"}}},{"parameters":{"resource":"orders","operation":"update","elementId":"={{ $json.orderId }}","parameters":{},"bodyType":["formBinaryData"],"requestOptions":{}},"id":"254a4383-97fc-43b2-aae0-45b5bccef54d","name":"Update order (attach raw file - per order)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3696,1456],"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":[3696,1264],"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":"={{ $('Limit').item.json.envelope?.subject || $('Limit').item.json.subject || '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] AI extraction could not parse line items. Raw email + attachments attached below.\n","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":[3248,3376],"credentials":{"pocketbaseHttpApi":{"id":"hLwB1QPIJEWsuVyW","name":"PocketBase HTTP account"}}},{"parameters":{"resource":"orders","operation":"update","elementId":"={{ $json.orderId }}","parameters":{},"bodyType":["formBinaryData"],"requestOptions":{}},"id":"bfd49b45-1d0e-4750-a775-bb3dbf24a0a8","name":"Update order (attach raw file - single)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[4144,3280],"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":[4144,2272]},{"parameters":{"options":{}},"id":"b4ce7c83-a9f2-4d5d-b6f9-92d572b6c2f1","name":"Attachment loop","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[4368,2272]},{"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}},"options":{}},"id":"88a759c4-8c13-43c7-b868-d243c166b836","name":"Has attachment to upload?","type":"n8n-nodes-base.if","typeVersion":2.3,"position":[4592,2144]},{"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":[3920,992]},{"parameters":{"options":{}},"id":"211984cb-52b7-47c5-aa76-09c6bdc12aa7","name":"Attachment loop (Multi)","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[4144,992]},{"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":[4368,992]},{"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":[3248,1904]},{"parameters":{"options":{}},"id":"bf6b5c3f-a8b9-49c7-aaa8-56d2d90f31e9","name":"Attachment loop (whole email)","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[3472,1904]},{"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":[3248,1184]},{"parameters":{"options":{}},"id":"58907510-2052-408f-85fe-2811aff80f32","name":"Attachment loop (per order)","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[3472,1184]},{"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":[3472,3376]},{"parameters":{"options":{}},"id":"fbd21c7d-4c30-4b36-b53d-a0bff29eb082","name":"Attachment loop (single fallback)","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[3696,3376]},{"parameters":{"model":"MiniMax-M2.5","options":{}},"type":"@n8n/n8n-nodes-langchain.lmChatMinimax","typeVersion":1,"position":[2024,1080],"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":[3920,3120]},{"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 — drop it silently\nif (Object.keys(cleanBinary).length === 0) {\n return [];\n}\n\nreturn [{\n json: item.json,\n binary: cleanBinary,\n}];"},"id":"f5d15e31-5ee7-49d4-a4e3-2a21e15bf2bf","name":"Sanitise binary for upload","type":"n8n-nodes-base.code","typeVersion":2,"position":[4816,2144]},{"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}},"id":"282149bd-070a-41dd-9c4b-468f1d0b6aa1","name":"SetEmailFlags email (mark on fetch)","type":"n8n-nodes-imap-enhanced.imapEnhanced","typeVersion":1,"position":[-2128,3744],"credentials":{"imapApi":{"id":"vPyqCkQOwcnLgJxZ","name":"ORDERS @ WFSALES"}},"disabled":true,"notes":"DISABLED 2026-08-24 — this node was stripping the email body (envelope/textContent/headers), causing the AI Agent downstream to receive empty prompts and If1 to route every email to MoveEmail email1 (INBOX.202608.Failed). Removed from chain. MoveEmail on success path handles the Seen flag implicitly via mailbox move; on failure the email stays Unseen and gets re-polled next cycle (free retry). See docs/audit/order-pipeline-implementation-2026-08-21.md for context."},{"parameters":{"resource":"orders","operation":"getList","parameters":{},"requestOptions":{}},"id":"0c8d53f6-057e-4dc2-b0ad-7add37ae26d3","name":"Search orders by email_message_id","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3248,2704],"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":[3472,2704]},{"parameters":{"jsCode":"// Branch hit when 'Search orders by email_message_id' returns ≥1 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 §2.\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":[7232,2976]},{"parameters":{"resource":"orders","operation":"getList","parameters":{},"requestOptions":{}},"id":"e9507973-bc42-4321-83e5-db03870ab9bd","name":"Search orders by email_message_id (Multi)","type":"n8n-nodes-pocketbase.pocketbaseHttp","typeVersion":1,"position":[3024,528],"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":[3248,528]},{"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":[3472,992]},{"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 — 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 §3.\n//\n// ──────────────────────────────────────────────────────────────────\n// 1. Email signature parse\n// ──────────────────────────────────────────────────────────────────\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// ──────────────────────────────────────────────────────────────────\n// 2. PDF \"Requester\" / \"Deliver To\" / \"Attention\" parse\n// ──────────────────────────────────────────────────────────────────\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 — fall back to email sig.\n}\n\n// ──────────────────────────────────────────────────────────────────\n// 3. Pick the best candidate (PDF > email signature > nothing)\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":"820c8fc4-4853-45b7-a0ae-1d934e21eddf","name":"Extract contact name (deterministic)","type":"n8n-nodes-base.code","typeVersion":2,"position":[1952,3200]},{"parameters":{"jsCode":"// Deterministic contact_name extraction (multi-branch).\n// Same algorithm as the single-branch 'Extract contact name\n// (deterministic)' — 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 §3.\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 — 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":[1584,640]},{"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 §3.\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 — 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 — 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":[2800,3200]},{"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":[2448,640]}],"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":"Classify Email (Deterministic)","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":[]},"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}]]}},"nodeGroups":[],"authors":"David Els","name":null,"description":null,"autosaved":false,"workflowPublishHistory":[{"createdAt":"2026-08-24T15:03:03.565Z","id":802,"workflowId":"DWkxko0SS1uoelsg","versionId":"57bf8616-0a62-4b7a-96c1-18ede2e55e0b","event":"deactivated","userId":"4f770df2-bd2f-4436-b153-b27004efc7b6"},{"createdAt":"2026-08-24T15:03:03.675Z","id":803,"workflowId":"DWkxko0SS1uoelsg","versionId":"57bf8616-0a62-4b7a-96c1-18ede2e55e0b","event":"activated","userId":"4f770df2-bd2f-4436-b153-b27004efc7b6"}]}}