{
  "nodes": [
    {
      "id": "55c614fc-b469-44f4-b501-716457a7fcce",
      "name": "CSV Upload Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -2096,
        224
      ],
      "parameters": {
        "path": "csv-upload",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "lastNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "72dec0b7-ae86-48ac-b700-b141198bd745",
      "name": "Workflow Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -1760,
        224
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "postgresTable",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Postgres table name__>"
            },
            {
              "id": "id-2",
              "name": "errorThreshold",
              "type": "number",
              "value": 0.05
            },
            {
              "id": "id-3",
              "name": "slackChannel",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Slack channel ID__>"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "81c17ab9-dcfe-4722-bec0-1be96785ccf7",
      "name": "Check File Type",
      "type": "n8n-nodes-base.if",
      "position": [
        -1600,
        224
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $binary.data.mimeType }}",
              "rightValue": "csv"
            },
            {
              "id": "id-2",
              "operator": {
                "type": "string",
                "operation": "endsWith"
              },
              "leftValue": "={{ $binary.data.fileName }}",
              "rightValue": ".csv"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "4acb6b44-47f0-4122-9416-3c51ef52bb14",
      "name": "Extract CSV Data",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -1344,
        128
      ],
      "parameters": {
        "options": {
          "includeEmptyCells": true
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "381d7195-36b0-4c16-89ee-5747e8505855",
      "name": "Error - Unsupported File Type",
      "type": "n8n-nodes-base.set",
      "position": [
        -1360,
        320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "error",
              "type": "string",
              "value": "Unsupported file type. Only CSV files are accepted."
            },
            {
              "id": "id-2",
              "name": "fileName",
              "type": "string",
              "value": "={{ $binary.data.fileName }}"
            },
            {
              "id": "id-3",
              "name": "mimeType",
              "type": "string",
              "value": "={{ $binary.data.mimeType }}"
            },
            {
              "id": "id-4",
              "name": "status",
              "type": "string",
              "value": "failed"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a3ccc159-930d-4903-9fed-6e95655620f0",
      "name": "Schema Inference & Header Normalization",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -1088,
        128
      ],
      "parameters": {
        "text": "=CSV data to analyze: {{ JSON.stringify($json) }}",
        "options": {
          "systemMessage": "You are a data engineering assistant specializing in CSV schema inference and normalization.\n\nYour task is to:\n1. Analyze the CSV data structure and infer the correct data types for each column (string, number, boolean, date, currency)\n2. Detect the original column headers and map them to canonical/standardized names (e.g., \"amt\" \u2192 \"amount\", \"cust_id\" \u2192 \"customer_id\")\n3. Identify any unit or currency inconsistencies within columns\n4. Detect null patterns and data quality issues\n5. Return a structured schema with: original_header, canonical_header, data_type, detected_issues, sample_values\n\nBe precise and deterministic in your analysis."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3
    },
    {
      "id": "1722f80b-d02b-4d11-b42a-7715e3adc295",
      "name": "Anthropic Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        -1088,
        448
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-5-20250929",
          "cachedResultName": "Claude Sonnet 4.5"
        },
        "options": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "e557957c-9ce1-4f85-bfbb-7546a7048ebb",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        -944,
        448
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"columns\": {\n\t\t\t\"type\": \"array\",\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"properties\": {\n\t\t\t\t\t\"original_header\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t},\n\t\t\t\t\t\"canonical_header\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t},\n\t\t\t\t\t\"data_type\": {\n\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t},\n\t\t\t\t\t\"detected_issues\": {\n\t\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"sample_values\": {\n\t\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\t\"items\": {\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"row_count\": {\n\t\t\t\"type\": \"number\"\n\t\t},\n\t\t\"quality_score\": {\n\t\t\t\"type\": \"number\"\n\t\t}\n\t}\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "aebf766d-9e13-468c-bf19-6c8d02ff4f56",
      "name": "Apply Normalization & Type Coercion",
      "type": "n8n-nodes-base.code",
      "position": [
        -736,
        128
      ],
      "parameters": {
        "jsCode": "// Apply Normalization & Type Coercion\n// This code applies the schema normalization from the AI agent\n\n// Get the schema from the AI agent output\nconst schemaOutput = $('Schema Inference & Header Normalization').first().json;\nconst schema = schemaOutput.schema || schemaOutput;\n\n// Get the raw CSV data\nconst csvData = $('Extract CSV Data').all();\n\n// Helper function to coerce values based on detected type\nfunction coerceValue(value, targetType, format = null) {\n  if (value === null || value === undefined || value === '') {\n    return null;\n  }\n  \n  switch (targetType.toLowerCase()) {\n    case 'number':\n    case 'integer':\n    case 'float':\n    case 'decimal':\n      // Remove currency symbols, commas, and other formatting\n      const numStr = String(value).replace(/[$,\u20ac\u00a3\u00a5]/g, '').trim();\n      const num = parseFloat(numStr);\n      return isNaN(num) ? null : num;\n      \n    case 'date':\n    case 'datetime':\n    case 'timestamp':\n      // Try to parse date\n      const date = new Date(value);\n      return isNaN(date.getTime()) ? null : date.toISOString();\n      \n    case 'boolean':\n      const lowerVal = String(value).toLowerCase().trim();\n      if (['true', 'yes', '1', 'y', 't'].includes(lowerVal)) return true;\n      if (['false', 'no', '0', 'n', 'f'].includes(lowerVal)) return false;\n      return null;\n      \n    case 'string':\n    case 'text':\n    default:\n      return String(value).trim();\n  }\n}\n\n// Helper function to standardize units\nfunction standardizeUnit(value, unit, targetUnit) {\n  if (!value || !unit || !targetUnit) return value;\n  \n  const conversions = {\n    // Length\n    'ft_to_m': 0.3048,\n    'in_to_cm': 2.54,\n    'mi_to_km': 1.60934,\n    // Weight\n    'lb_to_kg': 0.453592,\n    'oz_to_g': 28.3495,\n    // Temperature\n    'f_to_c': (f) => (f - 32) * 5/9,\n    'c_to_f': (c) => (c * 9/5) + 32\n  };\n  \n  const conversionKey = `${unit.toLowerCase()}_to_${targetUnit.toLowerCase()}`;\n  const conversion = conversions[conversionKey];\n  \n  if (typeof conversion === 'function') {\n    return conversion(value);\n  } else if (typeof conversion === 'number') {\n    return value * conversion;\n  }\n  \n  return value;\n}\n\n// Apply normalization to each row\nconst normalizedData = csvData.map(item => {\n  const originalData = item.json;\n  const normalizedRow = {};\n  \n  // If schema has field mappings, apply them\n  if (schema.fields && Array.isArray(schema.fields)) {\n    schema.fields.forEach(field => {\n      const originalName = field.originalName || field.name;\n      const canonicalName = field.canonicalName || field.name;\n      const dataType = field.type || 'string';\n      const format = field.format;\n      const unit = field.unit;\n      const targetUnit = field.targetUnit;\n      \n      // Get the original value\n      let value = originalData[originalName];\n      \n      // Coerce to target type\n      value = coerceValue(value, dataType, format);\n      \n      // Standardize units if needed\n      if (unit && targetUnit && value !== null) {\n        value = standardizeUnit(value, unit, targetUnit);\n      }\n      \n      // Set the normalized value with canonical name\n      normalizedRow[canonicalName] = value;\n    });\n  } else {\n    // If no schema mapping, just copy the data\n    Object.assign(normalizedRow, originalData);\n  }\n  \n  return { json: normalizedRow };\n});\n\n// Add metadata about the normalization\nconst output = normalizedData.map((item, index) => ({\n  json: {\n    ...item.json,\n    _metadata: {\n      rowNumber: index + 1,\n      normalizedAt: new Date().toISOString(),\n      schemaApplied: true\n    }\n  }\n}));\n\nreturn output;"
      },
      "typeVersion": 2
    },
    {
      "id": "abecb0db-7796-4bf7-9276-4d9c654eed7e",
      "name": "Validate Data Quality",
      "type": "n8n-nodes-base.code",
      "position": [
        -512,
        128
      ],
      "parameters": {
        "jsCode": "// Validate Data Quality: Check type consistency, detect outliers, validate ranges\n// Input: Normalized data from previous node\n// Output: Clean data + error report\n\nconst items = $input.all();\nconst cleanData = [];\nconst errorReport = [];\nlet totalRows = 0;\nlet errorCount = 0;\n\n// Get schema from previous node (if available)\nconst schema = items[0]?.json?.schema || {};\nconst normalizedData = items[0]?.json?.normalizedData || items.map(item => item.json);\n\n// Helper function to detect data type\nfunction detectType(value) {\n  if (value === null || value === undefined || value === '') return 'null';\n  if (!isNaN(value) && !isNaN(parseFloat(value))) return 'number';\n  if (value === 'true' || value === 'false' || typeof value === 'boolean') return 'boolean';\n  if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2}/.test(value)) return 'date';\n  return 'string';\n}\n\n// Helper function to detect outliers using IQR method\nfunction detectOutliers(values) {\n  const sorted = values.filter(v => !isNaN(v)).sort((a, b) => a - b);\n  if (sorted.length < 4) return new Set();\n  \n  const q1Index = Math.floor(sorted.length * 0.25);\n  const q3Index = Math.floor(sorted.length * 0.75);\n  const q1 = sorted[q1Index];\n  const q3 = sorted[q3Index];\n  const iqr = q3 - q1;\n  const lowerBound = q1 - 1.5 * iqr;\n  const upperBound = q3 + 1.5 * iqr;\n  \n  return new Set(values.map((v, i) => (v < lowerBound || v > upperBound) ? i : null).filter(i => i !== null));\n}\n\n// Analyze each column for type consistency and outliers\nconst columnStats = {};\nconst columns = Object.keys(normalizedData[0] || {});\n\ncolumns.forEach(col => {\n  const values = normalizedData.map(row => row[col]);\n  const types = values.map(v => detectType(v));\n  const typeCounts = types.reduce((acc, type) => {\n    acc[type] = (acc[type] || 0) + 1;\n    return acc;\n  }, {});\n  \n  const dominantType = Object.keys(typeCounts).reduce((a, b) => \n    typeCounts[a] > typeCounts[b] ? a : b\n  );\n  \n  columnStats[col] = {\n    dominantType,\n    typeCounts,\n    outlierIndices: dominantType === 'number' ? \n      detectOutliers(values.map(v => parseFloat(v))) : new Set()\n  };\n});\n\n// Validate each row\nnormalizedData.forEach((row, rowIndex) => {\n  totalRows++;\n  const rowErrors = [];\n  \n  columns.forEach(col => {\n    const value = row[col];\n    const stats = columnStats[col];\n    const actualType = detectType(value);\n    \n    // Check type consistency\n    if (actualType !== stats.dominantType && actualType !== 'null') {\n      rowErrors.push({\n        column: col,\n        issue: 'type_mismatch',\n        expected: stats.dominantType,\n        actual: actualType,\n        value: value\n      });\n    }\n    \n    // Check for outliers\n    if (stats.outlierIndices.has(rowIndex)) {\n      rowErrors.push({\n        column: col,\n        issue: 'outlier',\n        value: value,\n        message: 'Value is statistical outlier'\n      });\n    }\n    \n    // Check for empty required fields (assuming non-null is required)\n    if ((value === null || value === undefined || value === '') && stats.typeCounts.null < totalRows * 0.5) {\n      rowErrors.push({\n        column: col,\n        issue: 'missing_value',\n        message: 'Required field is empty'\n      });\n    }\n  });\n  \n  // Add to appropriate output\n  if (rowErrors.length > 0) {\n    errorCount++;\n    errorReport.push({\n      rowIndex: rowIndex + 1,\n      data: row,\n      errors: rowErrors,\n      errorCount: rowErrors.length\n    });\n  } else {\n    cleanData.push(row);\n  }\n});\n\n// Calculate error rate\nconst errorRate = totalRows > 0 ? (errorCount / totalRows * 100).toFixed(2) : 0;\n\n// Return results\nreturn [\n  {\n    json: {\n      summary: {\n        totalRows,\n        cleanRows: cleanData.length,\n        errorRows: errorCount,\n        errorRate: `${errorRate}%`,\n        timestamp: new Date().toISOString()\n      },\n      columnStats,\n      cleanData,\n      errorReport,\n      schema\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "83fc4ccf-8d59-423f-bf2c-866129ca05c3",
      "name": "Prepare Clean CSV Output",
      "type": "n8n-nodes-base.set",
      "position": [
        -288,
        32
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "cleanData",
              "type": "string",
              "value": "={{ $json.cleanData }}"
            },
            {
              "id": "id-2",
              "name": "rowsProcessed",
              "type": "number",
              "value": "={{ $json.rowsProcessed }}"
            },
            {
              "id": "id-3",
              "name": "errorRate",
              "type": "number",
              "value": "={{ $json.errorRate }}"
            },
            {
              "id": "id-4",
              "name": "status",
              "type": "string",
              "value": "success"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "86d9b91e-09a4-48f4-a354-6eac8b9768ef",
      "name": "Insert into Postgres",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -64,
        32
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Workflow Configuration').first().json.postgresTable }}"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": null,
          "mappingMode": "autoMapInputData"
        },
        "options": {}
      },
      "typeVersion": 2.6
    },
    {
      "id": "6997f71b-82f5-4069-b444-6ae915ec8529",
      "name": "Generate Error Report",
      "type": "n8n-nodes-base.set",
      "position": [
        -224,
        480
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "errorReport",
              "type": "string",
              "value": "={{ $json.errorReport }}"
            },
            {
              "id": "id-2",
              "name": "totalErrors",
              "type": "number",
              "value": "={{ $json.totalErrors }}"
            },
            {
              "id": "id-3",
              "name": "errorRate",
              "type": "number",
              "value": "={{ $json.errorRate }}"
            },
            {
              "id": "id-4",
              "name": "timestamp",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            },
            {
              "id": "id-5",
              "name": "fileName",
              "type": "string",
              "value": "={{ $('CSV Upload Webhook').first().binary.data.fileName }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "bf64d4a2-7ff0-4380-8e9a-d705e64cc339",
      "name": "Send Notification",
      "type": "n8n-nodes-base.slack",
      "position": [
        160,
        32
      ],
      "parameters": {
        "text": "=CSV processing completed successfully!\n\nRows processed: {{ $json.rowsProcessed }}\nError rate: {{ $json.errorRate }}%\nStatus: {{ $json.status }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Workflow Configuration').first().json.slackChannel }}"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.4
    },
    {
      "id": "1c5b987a-9054-4822-aef5-dfb990338980",
      "name": "Log to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -16,
        480
      ],
      "parameters": {
        "columns": {
          "value": {
            "fileName": "={{ $json.fileName }}",
            "errorRate": "={{ $json.errorRate }}",
            "timestamp": "={{ $json.timestamp }}",
            "errorReport": "={{ $json.errorReport }}",
            "totalErrors": "={{ $json.totalErrors }}"
          },
          "schema": [
            {
              "id": "fileName",
              "required": false,
              "displayName": "fileName",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "timestamp",
              "required": false,
              "displayName": "timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "totalErrors",
              "required": false,
              "displayName": "totalErrors",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "errorRate",
              "required": false,
              "displayName": "errorRate",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "errorReport",
              "required": false,
              "displayName": "errorReport",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "fileName"
          ]
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Error Log"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "<__PLACEHOLDER_VALUE__Google Sheets document ID__>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "20d8d06f-a9d4-4539-8a0b-c7790b21f437",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2176,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 320,
        "content": "## Upload Trigger\nReceives CSV file via webhook for processing."
      },
      "typeVersion": 1
    },
    {
      "id": "e69d373d-5786-4ef9-9d2a-be9698c58883",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 320,
        "content": "## Database Insert\nStores cleaned data into Postgres table and notify user"
      },
      "typeVersion": 1
    },
    {
      "id": "3dfd6dec-9db1-4414-a212-aaf171579352",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -272,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Error Handling\nGenerates error report for invalid or failed rows and Logs errors and reports into Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "81f4ae28-c4a5-44d6-a6dc-b5c90eda80d0",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Data Normalization and Validation\nCleans data, converts types, and standardizes formats and Checks quality, detects errors, missing values, and outliers."
      },
      "typeVersion": 1
    },
    {
      "id": "aecd603c-4d70-4bf8-8049-523ae5e3dd6d",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1136,
        -16
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 320,
        "content": "## Schema Detection\nAI infers schema, types, and normalizes column names."
      },
      "typeVersion": 1
    },
    {
      "id": "33ade988-85d3-4325-9a52-943bee03c93c",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1408,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 608,
        "content": "## CSV Extraction\nParses CSV file into structured rows for processing."
      },
      "typeVersion": 1
    },
    {
      "id": "3f2b7dee-3d87-42b2-ade8-1e5f2e24ce9b",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1792,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 320,
        "content": "## Configuration\nDefines Postgres table, error threshold, and Slack channel and Checks if uploaded file is a valid CSV format."
      },
      "typeVersion": 1
    },
    {
      "id": "2be20044-a5f7-4af4-89db-73772dc6912c",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2832,
        80
      ],
      "parameters": {
        "width": 496,
        "height": 464,
        "content": "## How it works\nThis workflow processes CSV files via webhook upload. It validates the file, extracts data, and uses AI to detect schema and standardize columns. The data is cleaned, normalized, and checked for errors like missing values or outliers. Clean data is stored in Postgres, while errors are logged and shared via Slack.\n\n## Setup\n1. Configure webhook endpoint for CSV upload  \n2. Set Postgres table name  \n3. Add Anthropic/OpenAI credentials  \n4. Connect Slack for notifications  \n5. Connect Google Sheets for error logs  \n6. Adjust error threshold settings  \n7. Test with sample CSV files  \n8. Activate the workflow"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Check File Type": {
      "main": [
        [
          {
            "node": "Extract CSV Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error - Unsupported File Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract CSV Data": {
      "main": [
        [
          {
            "node": "Schema Inference & Header Normalization",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CSV Upload Webhook": {
      "main": [
        [
          {
            "node": "Workflow Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Schema Inference & Header Normalization",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Insert into Postgres": {
      "main": [
        [
          {
            "node": "Send Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Error Report": {
      "main": [
        [
          {
            "node": "Log to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Data Quality": {
      "main": [
        [
          {
            "node": "Prepare Clean CSV Output",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate Error Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Configuration": {
      "main": [
        [
          {
            "node": "Check File Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Clean CSV Output": {
      "main": [
        [
          {
            "node": "Insert into Postgres",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Schema Inference & Header Normalization",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Error - Unsupported File Type": {
      "main": [
        [
          {
            "node": "Generate Error Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply Normalization & Type Coercion": {
      "main": [
        [
          {
            "node": "Validate Data Quality",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schema Inference & Header Normalization": {
      "main": [
        [
          {
            "node": "Apply Normalization & Type Coercion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}