{
  "id": "dxYXfkmTl5qRQ0IP",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Customs Document Verification & Compliance Checker",
  "tags": [],
  "nodes": [
    {
      "id": "1f530774-b99e-47a0-9960-844879a044d8",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2016,
        1520
      ],
      "parameters": {
        "color": 2,
        "width": 856,
        "height": 736,
        "content": "## \ud83e\uddfe Customs & Shipping Document Auto-Checker\n\n### How it works\nA freight forwarder or ops team POSTs three shipping documents \u2014 Commercial Invoice, Packing List, and Bill of Lading \u2014 as plain text to a webhook. GPT-4o-mini extracts structured fields from the BOL, a Code node cross-checks all three documents for mismatches, and Gmail sends a colour-coded compliance report back to the submitter. Any AI extraction failure routes straight to a Slack alert.\n\nManual cross-checking of these three documents is the single biggest source of customs delays. This workflow cuts that from hours to under two minutes.\n\n### Setup steps\n1. **OpenAI** \u2014 connect your OpenAI API credential to the BOL extraction node\n2. **Gmail** \u2014 connect Gmail OAuth2 to both report nodes; update the hardcoded `sendTo` address to use `$json.submitterEmail`\n3. **Slack** \u2014 connect your Slack OAuth2 credential to both error alert nodes and set the correct channel\n4. **Test** \u2014 demo data is pinned on the Webhook node; click the node \u2192 **Test step** to fire a full run without a real HTTP call\n5. **Go live** \u2014 activate the workflow and POST to the webhook URL with: `submitterEmail`, `shipmentRef`, `invoiceText`, `packingText`, `bolText`"
      },
      "typeVersion": 1
    },
    {
      "id": "1f03f81a-ae99-4c6b-90b9-ed23290d4a0b",
      "name": "Intake Section",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1008,
        2112
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 420,
        "content": "## \ud83d\udce1 Webhook Intake & Field Mapping\nReceives the POST payload and maps all five fields \u2014 email, shipment reference, and three document texts \u2014 into clean variables for downstream nodes. Demo data is pinned here for testing."
      },
      "typeVersion": 1
    },
    {
      "id": "4b331209-7fe9-422e-a74c-72ec9d174c9a",
      "name": "AI Extraction Section",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        2048
      ],
      "parameters": {
        "color": 7,
        "width": 444,
        "height": 620,
        "content": "## \ud83e\udd16 AI Document Extraction\nGPT-4o-mini reads the Bill of Lading and returns a structured JSON object. The error output routes failures to Slack so nothing fails silently. Extend this section with Invoice and Packing List nodes to enable full three-way cross-checking."
      },
      "typeVersion": 1
    },
    {
      "id": "69308c88-03ea-496c-be56-e7ff9680d851",
      "name": "Cross-Check Section",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        2032
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 588,
        "content": "## \ud83d\udd0d Cross-Check & Verdict\nRuns 10+ field comparisons across the three documents \u2014 consignee, shipper, ports, HS codes, quantities, package count, gross weight, and shipping marks. Outputs a verdict (`READY_TO_FILE` or `NEEDS_CORRECTION`) plus a full check log with individual check IDs."
      },
      "typeVersion": 1
    },
    {
      "id": "adc6ae8c-9bfb-4954-b76b-9c1b7b2cc33a",
      "name": "Email Reports Section",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        672,
        1920
      ],
      "parameters": {
        "color": 7,
        "width": 444,
        "height": 796,
        "content": "## \ud83d\udce7 Compliance Report Emails\nBranches on verdict. Green path sends a full passed-checks table. Red path sends a discrepancy table with check IDs, exact mismatched values, and an all-checks summary \u2014 everything the filer needs to make corrections before submission."
      },
      "typeVersion": 1
    },
    {
      "id": "261a19d8-7a7c-41cc-9ad1-0b752be555ea",
      "name": "Error Handler Section",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1072,
        2736
      ],
      "parameters": {
        "color": 7,
        "width": 892,
        "height": 500,
        "content": "## \u26a0\ufe0f Error Handling\nTwo Slack alerts catch failures \u2014 one on the workflow error trigger (catches any node), one on the AI extraction error output. Wire additional critical nodes to the Slack alert node to broaden coverage."
      },
      "typeVersion": 1
    },
    {
      "id": "fde9bd7b-9a7f-4594-b49b-8813a8cfc483",
      "name": "Credentials",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        2960
      ],
      "parameters": {
        "color": 3,
        "width": 400,
        "height": 232,
        "content": "## \ud83d\udd10 Credentials & Security\nOpenAI API key via Header Auth. Gmail and Slack via OAuth2. Replace all hardcoded email addresses with `$json.submitterEmail`. Never commit real API keys or Slack channel IDs to shared templates."
      },
      "typeVersion": 1
    },
    {
      "id": "b8bf84ea-1f05-4dc5-979b-4f01b0ebdb92",
      "name": "\ud83d\udce1 Webhook \u2014 Receive Documents",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -912,
        2336
      ],
      "parameters": {
        "path": "customs-doc-checker",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "lastNode"
      },
      "typeVersion": 2
    },
    {
      "id": "70a611aa-2691-4e9a-8605-087c83195f9c",
      "name": "Map Incoming Webhook Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        -672,
        2336
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "assign-email",
              "name": "submitterEmail",
              "type": "string",
              "value": "={{ $json.body.submitterEmail }}"
            },
            {
              "id": "assign-ref",
              "name": "shipmentRef",
              "type": "string",
              "value": "={{ $json.body.shipmentRef }}"
            },
            {
              "id": "assign-invoice-text",
              "name": "invoiceText",
              "type": "string",
              "value": "={{ $json.body.invoiceText }}"
            },
            {
              "id": "assign-packing-text",
              "name": "packingText",
              "type": "string",
              "value": "={{ $json.body.packingText }}"
            },
            {
              "id": "assign-bol-text",
              "name": "bolText",
              "type": "string",
              "value": "={{ $json.body.bolText }}"
            },
            {
              "id": "assign-timestamp",
              "name": "submittedAt",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f6757d19-0066-41c2-be27-d7251f2b9185",
      "name": "Extract Bill of Lading Fields via GPT",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "onError": "continueErrorOutput",
      "position": [
        -384,
        2336
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "GPT-4O-MINI"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "You are a customs compliance specialist. Extract structured data from shipping documents. Always respond with valid JSON only \u2014 no markdown, no backticks, no explanation. JSON only."
            },
            {
              "content": "=Extract the following fields from this BILL OF LADING and return as a single JSON object:\n\n- bol_number (string)\n- bol_date (string)\n- shipper_name (string)\n- consignee_name (string)\n- notify_party (string)\n- vessel_name (string)\n- voyage_number (string)\n- port_of_loading (string)\n- port_of_discharge (string)\n- place_of_delivery (string)\n- total_packages (number)\n- total_gross_weight_kg (number)\n- description_of_goods (string)\n- freight_terms (string)\n- shipping_marks (string)\n\nDocument text:\n{{ $('Map Incoming Webhook Fields').item.json.bolText }}"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c2de3add-b413-4007-9731-be2e1f74c7bb",
      "name": "Cross-Check All Document Fields",
      "type": "n8n-nodes-base.code",
      "position": [
        144,
        2336
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// CUSTOMS DOCUMENT CROSS-CHECKER\n// Compares Invoice \u2194 Packing List \u2194 Bill of Lading\n// Flags mismatches with line references\n// ============================================================\n\nconst inv = $input.item.json.invoiceParsed || {};\nconst pl  = $input.item.json.packingParsed  || {};\nconst bol = $input.item.json.bolParsed      || {};\n\nconst discrepancies = [];\nconst checks = [];\n\nfunction check(id, label, invVal, plVal, bolVal) {\n  const strInv = String(invVal || '').trim().toLowerCase();\n  const strPl  = String(plVal  || '').trim().toLowerCase();\n  const strBol = String(bolVal || '').trim().toLowerCase();\n\n  let mismatch = false;\n  const mismatches = [];\n\n  if (plVal !== undefined && strInv !== strPl) {\n    mismatch = true;\n    mismatches.push(`Invoice: \"${invVal}\" vs Packing List: \"${plVal}\"`);\n  }\n  if (bolVal !== undefined && strInv !== strBol) {\n    mismatch = true;\n    mismatches.push(`Invoice: \"${invVal}\" vs Bill of Lading: \"${bolVal}\"`);\n  }\n  if (plVal !== undefined && bolVal !== undefined && strPl !== strBol) {\n    mismatch = true;\n    mismatches.push(`Packing List: \"${plVal}\" vs Bill of Lading: \"${bolVal}\"`);\n  }\n\n  const result = { id, label, status: mismatch ? 'MISMATCH' : 'OK', details: mismatches.join(' | ') };\n  checks.push(result);\n  if (mismatch) discrepancies.push(result);\n}\n\ncheck('CON-001', 'Consignee / Buyer Name', inv.buyer_name, pl.consignee_name, bol.consignee_name);\ncheck('SHP-001', 'Shipper / Seller Name', inv.seller_name, pl.shipper_name, bol.shipper_name);\ncheck('PORT-001', 'Port of Loading', inv.port_of_loading, undefined, bol.port_of_loading);\ncheck('PORT-002', 'Port of Discharge', inv.port_of_discharge, undefined, bol.port_of_discharge);\n\nconst bolPackages = parseFloat(bol.total_packages) || 0;\nconst plTotalPackages = parseFloat(pl.total_packages) || 0;\nconst pkgMismatch = [];\nif (plTotalPackages > 0 && bolPackages > 0 && plTotalPackages !== bolPackages) {\n  pkgMismatch.push(`Packing List: ${plTotalPackages} cartons vs BOL: ${bolPackages} cartons`);\n}\nconst pkgResult = { id: 'PKG-001', label: 'Total Packages (PL vs BOL)', status: pkgMismatch.length ? 'MISMATCH' : 'OK', details: pkgMismatch.join(' | ') };\nchecks.push(pkgResult);\nif (pkgMismatch.length) discrepancies.push(pkgResult);\n\nconst plGW  = parseFloat((pl.total_gross_weight_kg  || '').toString().replace(/[^0-9.]/g, '')) || 0;\nconst bolGW = parseFloat((bol.total_gross_weight_kg || '').toString().replace(/[^0-9.]/g, '')) || 0;\nconst gwMismatch = [];\nif (plGW > 0 && bolGW > 0 && Math.abs(plGW - bolGW) > 1) {\n  gwMismatch.push(`Packing List: ${plGW} kg vs BOL: ${bolGW} kg`);\n}\nconst gwResult = { id: 'WGT-001', label: 'Total Gross Weight (PL vs BOL)', status: gwMismatch.length ? 'MISMATCH' : 'OK', details: gwMismatch.join(' | ') };\nchecks.push(gwResult);\nif (gwMismatch.length) discrepancies.push(gwResult);\n\nconst invItems = inv.line_items || [];\nconst plItems  = pl.line_items  || [];\nfor (let i = 0; i < Math.max(invItems.length, plItems.length); i++) {\n  const invItem = invItems[i];\n  const plItem  = plItems[i];\n  if (!invItem || !plItem) continue;\n\n  const invHs = String(invItem.hs_code || '').replace(/[^0-9]/g, '');\n  const plHs  = String(plItem.hs_code  || '').replace(/[^0-9]/g, '');\n  if (invHs && plHs && invHs !== plHs) {\n    const hsResult = { id: `HS-00${i+1}`, label: `HS Code \u2014 Line Item ${i+1} (Invoice vs Packing List)`, status: 'MISMATCH', details: `Invoice HS: ${invItem.hs_code} vs Packing List HS: ${plItem.hs_code} | Item: ${invItem.description || plItem.description}` };\n    checks.push(hsResult); discrepancies.push(hsResult);\n  } else {\n    checks.push({ id: `HS-00${i+1}`, label: `HS Code \u2014 Line Item ${i+1}`, status: 'OK', details: `Both documents show HS: ${invItem.hs_code}` });\n  }\n\n  const invQty = parseFloat(invItem.quantity) || 0;\n  const plQty  = parseFloat(plItem.quantity)  || 0;\n  if (invQty > 0 && plQty > 0 && invQty !== plQty) {\n    const qtyResult = { id: `QTY-00${i+1}`, label: `Quantity \u2014 Line Item ${i+1} (Invoice vs Packing List)`, status: 'MISMATCH', details: `Invoice: ${invQty} ${invItem.unit || ''} vs Packing List: ${plQty} ${plItem.unit || ''} | Item: ${invItem.description}` };\n    checks.push(qtyResult); discrepancies.push(qtyResult);\n  } else {\n    checks.push({ id: `QTY-00${i+1}`, label: `Quantity \u2014 Line Item ${i+1}`, status: 'OK', details: `Both documents: ${invQty} ${invItem.unit || 'pcs'}` });\n  }\n}\n\nconst plMarks  = String(pl.marks_and_numbers || '').trim().toLowerCase();\nconst bolMarks = String(bol.shipping_marks   || '').trim().toLowerCase();\nif (plMarks && bolMarks && plMarks !== bolMarks) {\n  const marksResult = { id: 'MRK-001', label: 'Shipping Marks (PL vs BOL)', status: 'MISMATCH', details: `Packing List: \"${pl.marks_and_numbers}\" vs BOL: \"${bol.shipping_marks}\"` };\n  checks.push(marksResult); discrepancies.push(marksResult);\n} else {\n  checks.push({ id: 'MRK-001', label: 'Shipping Marks', status: 'OK', details: '' });\n}\n\nconst verdict = discrepancies.length === 0 ? 'READY_TO_FILE' : 'NEEDS_CORRECTION';\nconst totalChecks = checks.length;\nconst passedChecks = checks.filter(c => c.status === 'OK').length;\n\nreturn {\n  json: {\n    shipmentRef:    $input.item.json.shipmentRef,\n    submitterEmail: $input.item.json.submitterEmail,\n    verdict,\n    summary: { total: totalChecks, passed: passedChecks, failed: discrepancies.length },\n    discrepancies,\n    allChecks: checks,\n    invoiceSummary: {\n      invoiceNumber:   inv.invoice_number,\n      invoiceDate:     inv.invoice_date,\n      seller:          inv.seller_name,\n      buyer:           inv.buyer_name,\n      totalValue:      inv.total_invoice_value,\n      currency:        inv.currency,\n      incoterms:       inv.incoterms,\n      countryOfOrigin: inv.country_of_origin\n    },\n    bolSummary: {\n      bolNumber:    bol.bol_number,\n      vessel:       bol.vessel_name,\n      voyage:       bol.voyage_number,\n      freightTerms: bol.freight_terms\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "9b21d109-fc03-4edc-a7ce-de15c14a500d",
      "name": "Check Verdict \u2014 Ready to File?",
      "type": "n8n-nodes-base.if",
      "position": [
        384,
        2336
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-verdict",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json['verdict'] }}",
              "rightValue": "READY_TO_FILE"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b4a9959d-5325-4aca-b803-dd4cfacf8364",
      "name": "Gmail \u2014 Send Ready to File Report",
      "type": "n8n-nodes-base.gmail",
      "position": [
        816,
        2160
      ],
      "parameters": {
        "sendTo": "={{ $json.submitterEmail }}",
        "message": "=<html>\n<body style=\"font-family: Arial, sans-serif; max-width: 680px; margin: 0 auto; color: #333;\">\n  <div style=\"background: #1a7f4b; padding: 24px; border-radius: 8px 8px 0 0;\">\n    <h1 style=\"color: white; margin: 0; font-size: 22px;\">\u2705 Customs Document Check \u2014 Ready to File</h1>\n    <p style=\"color: #d4f0e1; margin: 6px 0 0;\">All cross-checks passed. Your shipment is cleared for customs submission.</p>\n  </div>\n  <div style=\"background: #f8f8f8; padding: 24px; border: 1px solid #e0e0e0;\">\n    <table style=\"width: 100%; border-collapse: collapse; margin-bottom: 20px;\">\n      <tr><td style=\"padding: 6px 0; font-weight: bold; width: 180px;\">Shipment Reference</td><td>{{ $json.shipmentRef }}</td></tr>\n      <tr><td style=\"padding: 6px 0; font-weight: bold;\">BOL Number</td><td>{{ $json.bolSummary ? $json.bolSummary.bolNumber : 'N/A' }}</td></tr>\n      <tr><td style=\"padding: 6px 0; font-weight: bold;\">Vessel / Voyage</td><td>{{ $json.bolSummary ? $json.bolSummary.vessel + ' / ' + $json.bolSummary.voyage : 'N/A' }}</td></tr>\n      <tr><td style=\"padding: 6px 0; font-weight: bold;\">Freight Terms</td><td>{{ $json.bolSummary ? $json.bolSummary.freightTerms : 'N/A' }}</td></tr>\n    </table>\n    <h3 style=\"color: #1a7f4b; border-bottom: 2px solid #1a7f4b; padding-bottom: 6px;\">Check Summary: {{ $json.summary.passed }}/{{ $json.summary.total }} Passed \u2705</h3>\n    <table style=\"width: 100%; border-collapse: collapse; font-size: 13px;\">\n      <thead><tr style=\"background: #eaf5ef;\"><th style=\"text-align:left; padding: 8px; border: 1px solid #ccc;\">Check ID</th><th style=\"text-align:left; padding: 8px; border: 1px solid #ccc;\">Check</th><th style=\"text-align:left; padding: 8px; border: 1px solid #ccc;\">Status</th></tr></thead>\n      <tbody>{{ $json.allChecks.map(c => `<tr><td style=\"padding:7px 8px; border:1px solid #ddd;\">${c.id}</td><td style=\"padding:7px 8px; border:1px solid #ddd;\">${c.label}</td><td style=\"padding:7px 8px; border:1px solid #ddd; color:#1a7f4b; font-weight:bold;\">\u2705 OK</td></tr>`).join('') }}</tbody>\n    </table>\n    <p style=\"margin-top: 20px; font-size: 12px; color: #777;\">Generated by Customs Doc Auto-Checker | {{ $now.toFormat('dd MMM yyyy HH:mm') }} IST</p>\n  </div>\n</body>\n</html>",
        "options": {},
        "subject": "=\u2705 READY TO FILE \u2014 Shipment {{ $json.shipmentRef }} | All Documents Clear"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "0f91d4e4-d61b-433c-ba4c-feb4ee437de7",
      "name": "Gmail \u2014 Send Needs Correction Report",
      "type": "n8n-nodes-base.gmail",
      "position": [
        832,
        2528
      ],
      "parameters": {
        "sendTo": "={{ $json.submitterEmail }}",
        "message": "=<html>\n<body style=\"font-family: Arial, sans-serif; max-width: 680px; margin: 0 auto; color: #333;\">\n  <div style=\"background: #c0392b; padding: 24px; border-radius: 8px 8px 0 0;\">\n    <h1 style=\"color: white; margin: 0; font-size: 22px;\">\u26a0\ufe0f Customs Document Check \u2014 Needs Correction</h1>\n    <p style=\"color: #fde8e6; margin: 6px 0 0;\">{{ $json.summary.failed }} discrepancy/discrepancies found. Please correct before customs submission.</p>\n  </div>\n  <div style=\"background: #f8f8f8; padding: 24px; border: 1px solid #e0e0e0;\">\n    <table style=\"width: 100%; border-collapse: collapse; margin-bottom: 20px;\">\n      <tr><td style=\"padding: 6px 0; font-weight: bold; width: 180px;\">Shipment Reference</td><td>{{ $json.shipmentRef }}</td></tr>\n      <tr><td style=\"padding: 6px 0; font-weight: bold;\">BOL Number</td><td>{{ $json.bolSummary ? $json.bolSummary.bolNumber : 'N/A' }}</td></tr>\n    </table>\n    <h3 style=\"color: #c0392b; border-bottom: 2px solid #c0392b; padding-bottom: 6px;\">\ud83d\udea8 Discrepancies Requiring Action</h3>\n    <table style=\"width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 24px;\">\n      <thead><tr style=\"background: #fde8e6;\"><th style=\"text-align:left; padding: 8px; border: 1px solid #ccc;\">Check ID</th><th style=\"text-align:left; padding: 8px; border: 1px solid #ccc;\">Field</th><th style=\"text-align:left; padding: 8px; border: 1px solid #ccc;\">Issue</th></tr></thead>\n      <tbody>{{ $json.discrepancies.map(d => `<tr><td style=\"padding:7px 8px; border:1px solid #ddd; font-weight:bold; color:#c0392b;\">${d.id}</td><td style=\"padding:7px 8px; border:1px solid #ddd;\">${d.label}</td><td style=\"padding:7px 8px; border:1px solid #ddd;\">${d.details}</td></tr>`).join('') }}</tbody>\n    </table>\n    <h3 style=\"color: #555; border-bottom: 1px solid #ccc; padding-bottom: 6px;\">All Checks Summary</h3>\n    <table style=\"width: 100%; border-collapse: collapse; font-size: 13px;\">\n      <thead><tr style=\"background: #f0f0f0;\"><th style=\"text-align:left; padding: 8px; border: 1px solid #ccc;\">Check ID</th><th style=\"text-align:left; padding: 8px; border: 1px solid #ccc;\">Check</th><th style=\"text-align:left; padding: 8px; border: 1px solid #ccc;\">Result</th></tr></thead>\n      <tbody>{{ $json.allChecks.map(c => `<tr><td style=\"padding:7px 8px; border:1px solid #ddd;\">${c.id}</td><td style=\"padding:7px 8px; border:1px solid #ddd;\">${c.label}</td><td style=\"padding:7px 8px; border:1px solid #ddd; color:${c.status==='OK'?'#1a7f4b':'#c0392b'}; font-weight:bold;\">${c.status==='OK'?'\u2705 OK':'\u26a0\ufe0f MISMATCH'}</td></tr>`).join('') }}</tbody>\n    </table>\n    <p style=\"margin-top: 20px; font-size: 12px; color: #777;\">Generated by Customs Doc Auto-Checker | {{ $now.toFormat('dd MMM yyyy HH:mm') }} IST</p>\n  </div>\n</body>\n</html>",
        "options": {},
        "subject": "=\u26a0\ufe0f NEEDS CORRECTION \u2014 Shipment {{ $json.shipmentRef }} | {{ $json.summary.failed }} Discrepancy Found"
      },
      "typeVersion": 2.1
    },
    {
      "id": "ec07202e-ca21-4af0-8e53-f27c19bee34f",
      "name": "Slack \u2014 Send Workflow Error Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        -720,
        2976
      ],
      "parameters": {
        "text": "=\u26a0\ufe0f Customs Doc Checker \u2014 workflow error. Check execution log for details.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "your-alerts-channel"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    },
    {
      "id": "38d2aad3-afdf-4255-91ff-075420fc3e66",
      "name": "On Workflow Error",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -976,
        2976
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "aad35f4f-d5cd-4bad-8e62-74ca2a43424d",
      "name": "Slack \u2014 Alert on AI Extraction Failure",
      "type": "n8n-nodes-base.slack",
      "position": [
        -416,
        2960
      ],
      "parameters": {
        "text": "=\u26a0\ufe0f Customs Doc Checker \u2014 AI extraction failed for shipment {{ $('Map Incoming Webhook Fields').item.json.shipmentRef }}. Check OpenAI credential or input format.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "your-alerts-channel"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "cbd5b6fa-7f50-43e1-8a40-3632ee2c564e",
  "nodeGroups": [],
  "connections": {
    "On Workflow Error": {
      "main": [
        [
          {
            "node": "Slack \u2014 Send Workflow Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Incoming Webhook Fields": {
      "main": [
        [
          {
            "node": "Extract Bill of Lading Fields via GPT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cross-Check All Document Fields": {
      "main": [
        [
          {
            "node": "Check Verdict \u2014 Ready to File?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Verdict \u2014 Ready to File?": {
      "main": [
        [
          {
            "node": "Gmail \u2014 Send Ready to File Report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail \u2014 Send Needs Correction Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce1 Webhook \u2014 Receive Documents": {
      "main": [
        [
          {
            "node": "Map Incoming Webhook Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Bill of Lading Fields via GPT": {
      "main": [
        [
          {
            "node": "Cross-Check All Document Fields",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack \u2014 Alert on AI Extraction Failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}