{"workflow":{"id":14317,"name":"Generate production database schemas from Excel and CSV with OpenAI and LangChain","views":48,"recentViews":2,"totalViews":48,"createdAt":"2026-03-25T14:54:09.295Z","description":"## Overview\n\nThis workflow automatically converts CSV or Excel files into a production-ready database schema using AI and rule-based validation.\n\nIt analyzes uploaded data, detects column types, relationships, and data quality, then generates a normalized schema. The output includes SQL DDL scripts, ERD diagrams, a data dictionary, and a load plan.\n\nThis eliminates manual schema design and accelerates database setup from raw data.\n\n---\n\n## How It Works\n\n1. **File Upload (Webhook)**\n   - Accepts CSV or XLSX files via webhook endpoint\n   - Initializes workflow configuration (thresholds, retry limits)\n\n2. **File Extraction**\n   - Detects file format (CSV or Excel)\n   - Extracts rows into structured JSON\n   - Merges extracted datasets\n\n3. **Data Cleaning & Profiling**\n   - Removes duplicates and normalizes values\n   - Detects data types (integer, float, date, boolean, string)\n   - Computes column statistics (nulls, uniqueness, distributions)\n   - Generates file hash and sample dataset\n\n4. **Column Profiling Engine**\n   - Identifies potential primary keys\n   - Detects cardinality and uniqueness levels\n   - Suggests foreign key relationships based on value overlap\n\n5. **AI Schema Generation**\n   - Uses an AI agent to design normalized tables\n   - Assigns SQL data types based on real data\n   - Defines primary keys, foreign keys, constraints, and indexes\n\n6. **Validation Layer**\n   - Ensures schema matches actual data\n   - Validates:\n     - Data types\n     - Primary key uniqueness\n     - Foreign key overlap (&gt;70%)\n     - Constraint consistency\n   - Detects circular dependencies\n\n7. **Revision Loop**\n   - If validation fails:\n     - Sends feedback to AI agent\n     - Regenerates schema\n     - Retries up to configured limit\n\n8. **Schema Output Generation**\n   - Generates:\n     - SQL DDL scripts\n     - ERD (Mermaid format)\n     - Data dictionary\n     - Load plan with dependency graph\n\n9. **Load Plan Engine**\n   - Computes optimal table insertion order\n   - Detects circular dependencies\n   - Suggests batching strategy\n\n10. **Combine & Explain**\n   - Merges all outputs\n   - Optional AI explanation of schema decisions\n\n11. **Response Output**\n   - Returns structured JSON via webhook:\n     - SQL schema\n     - ERD summary\n     - Data dictionary\n     - Load plan\n     - Optional explanation\n\n---\n\n## Setup Instructions\n\n1. Activate the workflow and copy the webhook URL  \n2. Send a POST request with a CSV or XLSX file  \n3. Configure OpenAI credentials (used by AI agent)  \n4. Adjust thresholds if needed (FK overlap, retries, confidence)  \n5. Execute workflow and review generated outputs  \n\n---\n\n## Use Cases\n\n- Auto-generate database schema from CSV/Excel files  \n- Data migration and onboarding pipelines  \n- Rapid database prototyping  \n- Reverse engineering datasets  \n- AI-assisted data modeling  \n\n---\n\n## Requirements\n\n- n8n (latest version recommended)  \n- OpenAI API credentials  \n- LangChain nodes enabled  \n- CSV or XLSX input file  ","workflow":{"meta":{"instanceId":"48aac30adfc5487a33ef16e0e958096f27cef40df3ed0febcbe1ca199e789786"},"nodes":[{"id":"9b4e0000-40d0-41b2-add6-a781e4ad10f0","name":"File Upload Webhook","type":"n8n-nodes-base.webhook","position":[-2480,64],"webhookId":"9904117a-327d-4b9d-a879-a013345254b0","parameters":{"path":"schema-generator","options":{},"httpMethod":"POST","responseMode":"lastNode"},"typeVersion":2.1},{"id":"6eed8414-4724-44b4-bcfe-bae6a3664880","name":"Workflow Configuration","type":"n8n-nodes-base.set","position":[-2224,64],"parameters":{"options":{},"assignments":{"assignments":[{"id":"id-1","name":"maxRevisionAttempts","type":"number","value":3},{"id":"id-2","name":"minForeignKeyOverlap","type":"number","value":0.7},{"id":"id-3","name":"confidenceThreshold","type":"number","value":0.8}]},"includeOtherFields":true},"typeVersion":3.4},{"id":"4d95d201-0631-4e5b-9cfd-5b109ade4a19","name":"Check File Type","type":"n8n-nodes-base.if","position":[-1904,64],"parameters":{"options":{},"conditions":{"options":{"leftValue":"","caseSensitive":false,"typeValidation":"loose"},"combinator":"and","conditions":[{"id":"id-1","operator":{"type":"boolean","operation":"true"},"leftValue":"={{ Object.keys($binary)[0].toLowerCase().includes('xls') }}"}]}},"typeVersion":2.3},{"id":"df4bbefc-a934-41be-a397-e3549a9616e5","name":"Extract Excel Data","type":"n8n-nodes-base.extractFromFile","position":[-1680,-16],"parameters":{"options":{"rawData":false,"headerRow":true,"sheetName":"","readAsString":false,"includeEmptyCells":false},"operation":"xlsx"},"typeVersion":1.1},{"id":"dc7d4633-54af-4f1a-9813-f02f63336d78","name":"Extract CSV Data","type":"n8n-nodes-base.extractFromFile","position":[-1680,192],"parameters":{"options":{"includeEmptyCells":true}},"typeVersion":1.1},{"id":"dc93fd9e-bf72-4bc4-bdcc-740ca9adf65f","name":"Merge Extracted Data","type":"n8n-nodes-base.merge","position":[-1456,64],"parameters":{"mode":"combine","options":{},"combineBy":"combineAll"},"typeVersion":3.2},{"id":"62fa98bb-0c4f-4edf-a48c-b1e9f1c3a6cf","name":"Compute File Hash & Profile Data","type":"n8n-nodes-base.code","position":[-1168,64],"parameters":{"jsCode":"const crypto = require('crypto');\n\n// Get input items (extracted data from previous node)\nconst items = $input.all();\n\nif (!items || items.length === 0) {\n  return [{ json: { error: 'No input data received' } }];\n}\n\n// Extract the data array from the first item\nconst rawData = items[0].json.data || items[0].json || [];\n\nif (!Array.isArray(rawData) || rawData.length === 0) {\n  return [{ json: { error: 'No valid data array found' } }];\n}\n\n// Compute file hash from raw data\nconst dataString = JSON.stringify(rawData);\nconst fileHash = crypto.createHash('sha256').update(dataString).digest('hex');\n\n// Helper function to normalize values\nfunction normalizeValue(value) {\n  if (value === null || value === undefined || value === '') {\n    return null;\n  }\n  \n  // Convert to string for processing\n  let strValue = String(value).trim();\n  \n  // Normalize comma-separated numbers (e.g., \"1,234.56\" -> 1234.56)\n  if (/^[\\d,]+\\.?\\d*$/.test(strValue)) {\n    const numValue = parseFloat(strValue.replace(/,/g, ''));\n    if (!isNaN(numValue)) {\n      return numValue;\n    }\n  }\n  \n  // Detect and normalize date strings\n  const datePatterns = [\n    /^\\d{4}-\\d{2}-\\d{2}$/,  // YYYY-MM-DD\n    /^\\d{2}\\/\\d{2}\\/\\d{4}$/,  // MM/DD/YYYY\n    /^\\d{2}-\\d{2}-\\d{4}$/,  // DD-MM-YYYY\n  ];\n  \n  for (const pattern of datePatterns) {\n    if (pattern.test(strValue)) {\n      const date = new Date(strValue);\n      if (!isNaN(date.getTime())) {\n        return date.toISOString();\n      }\n    }\n  }\n  \n  return value;\n}\n\n// Helper function to detect value type\nfunction detectType(value) {\n  if (value === null || value === undefined || value === '') {\n    return 'null';\n  }\n  \n  if (typeof value === 'number') {\n    return Number.isInteger(value) ? 'integer' : 'float';\n  }\n  \n  if (typeof value === 'boolean') {\n    return 'boolean';\n  }\n  \n  const strValue = String(value).trim();\n  \n  // Check for number\n  if (/^-?[\\d,]+\\.?\\d*$/.test(strValue)) {\n    const num = parseFloat(strValue.replace(/,/g, ''));\n    if (!isNaN(num)) {\n      return Number.isInteger(num) ? 'integer' : 'float';\n    }\n  }\n  \n  // Check for date\n  const date = new Date(strValue);\n  if (!isNaN(date.getTime()) && strValue.length > 5) {\n    return 'date';\n  }\n  \n  // Check for boolean-like strings\n  if (['true', 'false', 'yes', 'no', 'y', 'n'].includes(strValue.toLowerCase())) {\n    return 'boolean';\n  }\n  \n  return 'string';\n}\n\n// Detect headers\nconst firstRow = rawData[0];\nconst keys = Object.keys(firstRow);\n\n// Check for header inconsistencies\nconst headerCandidates = keys.map(key => {\n  return {\n    original: key,\n    normalized: key.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_'),\n    hasSpecialChars: /[^a-zA-Z0-9_\\s]/.test(key),\n    isEmpty: key.trim() === ''\n  };\n});\n\n// Normalize data and collect statistics\nconst normalizedData = [];\nconst columnStats = {};\nconst rowHashes = new Set();\nlet duplicateCount = 0;\n\nfor (const key of keys) {\n  columnStats[key] = {\n    types: {},\n    missingCount: 0,\n    totalCount: rawData.length,\n    uniqueValues: new Set(),\n    mixedTypes: false\n  };\n}\n\nfor (let i = 0; i < rawData.length; i++) {\n  const row = rawData[i];\n  const normalizedRow = {};\n  \n  // Create row hash for duplicate detection\n  const rowHash = crypto.createHash('md5').update(JSON.stringify(row)).digest('hex');\n  if (rowHashes.has(rowHash)) {\n    duplicateCount++;\n    continue; // Skip duplicate rows\n  }\n  rowHashes.add(rowHash);\n  \n  for (const key of keys) {\n    const rawValue = row[key];\n    const normalizedValue = normalizeValue(rawValue);\n    normalizedRow[key] = normalizedValue;\n    \n    // Update statistics\n    const stats = columnStats[key];\n    \n    if (normalizedValue === null) {\n      stats.missingCount++;\n    } else {\n      const type = detectType(normalizedValue);\n      stats.types[type] = (stats.types[type] || 0) + 1;\n      stats.uniqueValues.add(String(normalizedValue));\n    }\n  }\n  \n  normalizedData.push(normalizedRow);\n}\n\n// Determine type candidates and detect mixed types\nfor (const key of keys) {\n  const stats = columnStats[key];\n  const typeKeys = Object.keys(stats.types);\n  \n  if (typeKeys.length > 1) {\n    stats.mixedTypes = true;\n  }\n  \n  // Determine primary type (most common)\n  let primaryType = 'string';\n  let maxCount = 0;\n  \n  for (const [type, count] of Object.entries(stats.types)) {\n    if (count > maxCount) {\n      maxCount = count;\n      primaryType = type;\n    }\n  }\n  \n  stats.primaryType = primaryType;\n  stats.uniqueCount = stats.uniqueValues.size;\n  delete stats.uniqueValues; // Remove set for JSON serialization\n}\n\n// Get clean sample rows (first 100)\nconst sampleRows = normalizedData.slice(0, 100);\n\n// Prepare output\nconst output = {\n  fileHash,\n  totalRows: rawData.length,\n  cleanRows: normalizedData.length,\n  duplicateRows: duplicateCount,\n  headerCandidates,\n  columnStatistics: columnStats,\n  sampleRows,\n  summary: {\n    columnsWithMixedTypes: keys.filter(k => columnStats[k].mixedTypes),\n    columnsWithMissingValues: keys.filter(k => columnStats[k].missingCount > 0),\n    headerIssues: headerCandidates.filter(h => h.hasSpecialChars || h.isEmpty)\n  }\n};\n\nreturn [{ json: output }];"},"typeVersion":2},{"id":"24d0205d-e64f-4f35-b96a-ba46828840e1","name":"Column Profiling Engine","type":"n8n-nodes-base.code","position":[-1008,64],"parameters":{"jsCode":"// Column Profiling Engine\n// Profiles each column: nulls, types, uniqueness, cardinality, sequences, IDs, foreign keys\n\nconst items = $input.all();\n\nif (!items || items.length === 0) {\n  return [{ json: { error: 'No input data to profile' } }];\n}\n\n// Helper function to detect data type\nfunction detectType(value) {\n  if (value === null || value === undefined || value === '') return 'null';\n  if (typeof value === 'number') return isNaN(value) ? 'null' : 'number';\n  if (typeof value === 'boolean') return 'boolean';\n  \n  const str = String(value).trim();\n  if (str === '') return 'null';\n  \n  // Check for number\n  if (!isNaN(str) && !isNaN(parseFloat(str))) return 'number';\n  \n  // Check for date\n  const datePatterns = [\n    /^\\d{4}-\\d{2}-\\d{2}$/,\n    /^\\d{2}\\/\\d{2}\\/\\d{4}$/,\n    /^\\d{4}\\/\\d{2}\\/\\d{2}$/\n  ];\n  if (datePatterns.some(pattern => pattern.test(str))) return 'date';\n  \n  // Check for email\n  if (/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(str)) return 'email';\n  \n  // Check for URL\n  if (/^https?:\\/\\/.+/.test(str)) return 'url';\n  \n  return 'string';\n}\n\n// Helper function to check if sequence is monotonic\nfunction isMonotonic(values) {\n  if (values.length < 2) return false;\n  \n  let increasing = true;\n  let decreasing = true;\n  \n  for (let i = 1; i < values.length; i++) {\n    if (values[i] <= values[i - 1]) increasing = false;\n    if (values[i] >= values[i - 1]) decreasing = false;\n  }\n  \n  return increasing || decreasing;\n}\n\n// Extract all column names from all items\nconst allColumns = new Set();\nitems.forEach(item => {\n  Object.keys(item.json).forEach(col => allColumns.add(col));\n});\n\n// Profile each column\nconst columnProfiles = {};\n\nallColumns.forEach(columnName => {\n  const values = items.map(item => item.json[columnName]);\n  const totalCount = values.length;\n  \n  // Count nulls\n  const nullCount = values.filter(v => v === null || v === undefined || v === '').length;\n  const nullPercentage = (nullCount / totalCount) * 100;\n  \n  // Detect type distribution\n  const typeDistribution = {};\n  values.forEach(value => {\n    const type = detectType(value);\n    typeDistribution[type] = (typeDistribution[type] || 0) + 1;\n  });\n  \n  // Get dominant type\n  const dominantType = Object.entries(typeDistribution)\n    .filter(([type]) => type !== 'null')\n    .sort((a, b) => b[1] - a[1])[0]?.[0] || 'unknown';\n  \n  // Count unique values\n  const nonNullValues = values.filter(v => v !== null && v !== undefined && v !== '');\n  const uniqueValues = new Set(nonNullValues.map(v => String(v)));\n  const uniqueCount = uniqueValues.size;\n  const uniquePercentage = nonNullValues.length > 0 ? (uniqueCount / nonNullValues.length) * 100 : 0;\n  \n  // Infer cardinality level\n  let cardinalityLevel = 'unknown';\n  if (uniqueCount === 0) {\n    cardinalityLevel = 'empty';\n  } else if (uniqueCount === 1) {\n    cardinalityLevel = 'constant';\n  } else if (uniqueCount === nonNullValues.length) {\n    cardinalityLevel = 'unique';\n  } else if (uniqueCount <= 10) {\n    cardinalityLevel = 'low';\n  } else if (uniquePercentage < 5) {\n    cardinalityLevel = 'low';\n  } else if (uniquePercentage < 50) {\n    cardinalityLevel = 'medium';\n  } else {\n    cardinalityLevel = 'high';\n  }\n  \n  // Check if potential ID\n  const isPotentialId = (\n    uniqueCount === nonNullValues.length &&\n    uniqueCount > 0 &&\n    (columnName.toLowerCase().includes('id') ||\n     columnName.toLowerCase().includes('key') ||\n     columnName.toLowerCase().includes('code'))\n  );\n  \n  // Check if monotonic (for numeric values)\n  let isMonotonicSequence = false;\n  if (dominantType === 'number') {\n    const numericValues = nonNullValues\n      .map(v => parseFloat(v))\n      .filter(v => !isNaN(v))\n      .sort((a, b) => a - b);\n    isMonotonicSequence = isMonotonic(numericValues);\n  }\n  \n  // Store profile\n  columnProfiles[columnName] = {\n    columnName,\n    totalCount,\n    nullCount,\n    nullPercentage: Math.round(nullPercentage * 100) / 100,\n    typeDistribution,\n    dominantType,\n    uniqueCount,\n    uniquePercentage: Math.round(uniquePercentage * 100) / 100,\n    cardinalityLevel,\n    isPotentialId,\n    isMonotonic: isMonotonicSequence,\n    sampleValues: Array.from(uniqueValues).slice(0, 5)\n  };\n});\n\n// Detect foreign key candidates by comparing patterns across columns\nconst foreignKeyCandidates = {};\n\nconst columnNames = Object.keys(columnProfiles);\nfor (let i = 0; i < columnNames.length; i++) {\n  const col1 = columnNames[i];\n  const profile1 = columnProfiles[col1];\n  \n  // Skip if not a potential FK (must have reasonable cardinality)\n  if (profile1.cardinalityLevel === 'constant' || profile1.cardinalityLevel === 'empty') continue;\n  \n  const candidates = [];\n  \n  for (let j = 0; j < columnNames.length; j++) {\n    if (i === j) continue;\n    \n    const col2 = columnNames[j];\n    const profile2 = columnProfiles[col2];\n    \n    // Check if col1 values are subset of col2 values (FK relationship)\n    const values1 = new Set(items.map(item => String(item.json[col1])).filter(v => v && v !== 'null' && v !== 'undefined'));\n    const values2 = new Set(items.map(item => String(item.json[col2])).filter(v => v && v !== 'null' && v !== 'undefined'));\n    \n    if (values1.size === 0 || values2.size === 0) continue;\n    \n    // Calculate overlap\n    const intersection = new Set([...values1].filter(v => values2.has(v)));\n    const overlapPercentage = (intersection.size / values1.size) * 100;\n    \n    // If significant overlap and col2 has higher cardinality or is unique\n    if (overlapPercentage > 70 && \n        (profile2.isPotentialId || profile2.cardinalityLevel === 'unique' || profile2.uniqueCount >= profile1.uniqueCount)) {\n      candidates.push({\n        referencedColumn: col2,\n        overlapPercentage: Math.round(overlapPercentage * 100) / 100,\n        matchingValues: intersection.size\n      });\n    }\n  }\n  \n  if (candidates.length > 0) {\n    foreignKeyCandidates[col1] = candidates;\n  }\n}\n\n// Add foreign key candidates to profiles\nObject.keys(columnProfiles).forEach(col => {\n  columnProfiles[col].foreignKeyCandidates = foreignKeyCandidates[col] || [];\n});\n\n// Return comprehensive statistics\nreturn [{\n  json: {\n    columnProfiles,\n    summary: {\n      totalColumns: allColumns.size,\n      totalRows: items.length,\n      potentialIdColumns: Object.values(columnProfiles).filter(p => p.isPotentialId).map(p => p.columnName),\n      foreignKeyRelationships: Object.keys(foreignKeyCandidates).length,\n      profiledAt: new Date().toISOString()\n    },\n    rawData: items.map(item => item.json)\n  }\n}];"},"typeVersion":2},{"id":"c8f729ae-0c03-43df-b1bc-5c9513fc8929","name":"Schema Reasoning Agent","type":"@n8n/n8n-nodes-langchain.agent","position":[-768,64],"parameters":{"text":"={{ $json.cleanSampleRows }} with column statistics: {{ $json.columnStats }}","options":{"systemMessage":"You are a database schema architect. Your task is to analyze data profiling results and generate a production-grade database schema.\n\nYou will receive:\n- Clean sample rows from Excel/CSV files\n- Column statistics (nulls, types, uniqueness, cardinality, potential IDs, FK candidates)\n- Header candidates and type suggestions\n\nYour responsibilities:\n1. Normalize all headers to canonical column names (snake_case, descriptive)\n2. Propose correct SQL types based on actual data (not assumptions)\n3. Group columns into logical tables following normalization principles\n4. Decide primary keys (must be unique in data)\n5. Decide foreign keys (only if >70% key overlap exists)\n6. Decide which columns should be indexed\n7. Decide nullable vs NOT NULL based on actual null counts\n8. Add check constraints based on value ranges\n\nCRITICAL RULES:\n- You CANNOT create columns not seen in the input data\n- You CANNOT invent values or relationships without evidence\n- You MUST base all decisions on the provided statistics\n- Primary keys MUST have 100% uniqueness in the data\n- Foreign keys require >70% overlapping values between tables\n\nReturn a complete schema proposal with tables, columns, types, constraints, and relationships."},"promptType":"define","hasOutputParser":true},"typeVersion":3},{"id":"baa3d410-b8c9-453c-851c-ed3500a80c29","name":"Schema Output Parser","type":"@n8n/n8n-nodes-langchain.outputParserStructured","position":[-608,240],"parameters":{"schemaType":"manual","inputSchema":"{\n  \"type\": \"object\",\n  \"properties\": {\n    \"tables\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"table_name\": {\n            \"type\": \"string\"\n          },\n          \"columns\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"type\": \"string\"\n                },\n                \"sql_type\": {\n                  \"type\": \"string\"\n                },\n                \"nullable\": {\n                  \"type\": \"boolean\"\n                },\n                \"is_primary_key\": {\n                  \"type\": \"boolean\"\n                },\n                \"is_foreign_key\": {\n                  \"type\": \"boolean\"\n                },\n                \"references_table\": {\n                  \"type\": \"string\"\n                },\n                \"references_column\": {\n                  \"type\": \"string\"\n                },\n                \"check_constraint\": {\n                  \"type\": \"string\"\n                },\n                \"description\": {\n                  \"type\": \"string\"\n                }\n              },\n              \"required\": [\"name\", \"sql_type\", \"nullable\"]\n            }\n          },\n          \"primary_keys\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"foreign_keys\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"from_table\": {\n                  \"type\": \"string\"\n                },\n                \"from_column\": {\n                  \"type\": \"string\"\n                },\n                \"to_table\": {\n                  \"type\": \"string\"\n                },\n                \"to_column\": {\n                  \"type\": \"string\"\n                }\n              },\n              \"required\": [\"from_table\", \"from_column\", \"to_table\", \"to_column\"]\n            }\n          },\n          \"indexes\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"table_name\": {\n                  \"type\": \"string\"\n                },\n                \"columns\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"index_type\": {\n                  \"type\": \"string\"\n                }\n              },\n              \"required\": [\"table_name\", \"columns\"]\n            }\n          }\n        },\n        \"required\": [\"table_name\", \"columns\"]\n      }\n    },\n    \"reasoning\": {\n      \"type\": \"string\",\n      \"description\": \"Explanation of schema design decisions\"\n    }\n  },\n  \"required\": [\"tables\", \"reasoning\"]\n}"},"typeVersion":1.3},{"id":"d8d13598-9cbc-48c6-abc1-72205dd4cebf","name":"Rules Validation Layer","type":"n8n-nodes-base.code","position":[-416,64],"parameters":{"jsCode":"// Rules Validation Layer\n// Validates the schema against business rules and data constraints\n\nconst items = $input.all();\nconst schemaData = items[0].json;\n\n// Extract schema and configuration\nconst schema = schemaData.schema || schemaData;\nconst config = schemaData.config || {};\nconst profileData = schemaData.profileData || {};\nconst rawData = schemaData.rawData || [];\n\n// Get minForeignKeyOverlap from config (default 0.7 = 70%)\nconst minForeignKeyOverlap = config.minForeignKeyOverlap || 0.7;\n\nconst errors = [];\nconst warnings = [];\n\n// Helper function to get column values from raw data\nfunction getColumnValues(columnName) {\n  if (!rawData || rawData.length === 0) return [];\n  return rawData.map(row => row[columnName]).filter(val => val !== null && val !== undefined && val !== '');\n}\n\n// Helper function to check for duplicates\nfunction hasDuplicates(values) {\n  const seen = new Set();\n  for (const val of values) {\n    if (seen.has(val)) return true;\n    seen.add(val);\n  }\n  return false;\n}\n\n// Helper function to infer decimal precision from data\nfunction inferDecimalPrecision(values) {\n  let maxPrecision = 0;\n  let maxScale = 0;\n  \n  for (const val of values) {\n    const numStr = String(val);\n    if (numStr.includes('.')) {\n      const parts = numStr.split('.');\n      const precision = numStr.replace('.', '').replace('-', '').length;\n      const scale = parts[1].length;\n      maxPrecision = Math.max(maxPrecision, precision);\n      maxScale = Math.max(maxScale, scale);\n    } else {\n      const precision = numStr.replace('-', '').length;\n      maxPrecision = Math.max(maxPrecision, precision);\n    }\n  }\n  \n  return { precision: maxPrecision, scale: maxScale };\n}\n\n// Helper function to check type compatibility\nfunction checkTypeMatch(columnName, declaredType, values) {\n  if (values.length === 0) return true;\n  \n  const typeChecks = {\n    'INTEGER': (v) => Number.isInteger(Number(v)),\n    'BIGINT': (v) => Number.isInteger(Number(v)),\n    'SMALLINT': (v) => Number.isInteger(Number(v)),\n    'DECIMAL': (v) => !isNaN(Number(v)),\n    'NUMERIC': (v) => !isNaN(Number(v)),\n    'FLOAT': (v) => !isNaN(Number(v)),\n    'DOUBLE': (v) => !isNaN(Number(v)),\n    'VARCHAR': (v) => typeof v === 'string' || v !== null,\n    'TEXT': (v) => typeof v === 'string' || v !== null,\n    'DATE': (v) => !isNaN(Date.parse(v)),\n    'TIMESTAMP': (v) => !isNaN(Date.parse(v)),\n    'BOOLEAN': (v) => typeof v === 'boolean' || v === 'true' || v === 'false' || v === 0 || v === 1\n  };\n  \n  const baseType = declaredType.split('(')[0].toUpperCase();\n  const checker = typeChecks[baseType];\n  \n  if (!checker) return true; // Unknown type, skip validation\n  \n  const mismatches = values.filter(v => !checker(v));\n  return mismatches.length === 0;\n}\n\n// Helper function to get value range\nfunction getValueRange(values) {\n  const numericValues = values.map(v => Number(v)).filter(v => !isNaN(v));\n  if (numericValues.length === 0) return null;\n  return {\n    min: Math.min(...numericValues),\n    max: Math.max(...numericValues)\n  };\n}\n\n// Validation 1: Check all columns exist in input data\nif (schema.tables && Array.isArray(schema.tables)) {\n  const availableColumns = rawData.length > 0 ? Object.keys(rawData[0]) : [];\n  \n  for (const table of schema.tables) {\n    if (table.columns && Array.isArray(table.columns)) {\n      for (const column of table.columns) {\n        if (!availableColumns.includes(column.name)) {\n          errors.push(`Column '${column.name}' in table '${table.name}' does not exist in input data`);\n        }\n      }\n    }\n  }\n}\n\n// Validation 2: Check types match actual data\nif (schema.tables && Array.isArray(schema.tables)) {\n  for (const table of schema.tables) {\n    if (table.columns && Array.isArray(table.columns)) {\n      for (const column of table.columns) {\n        const values = getColumnValues(column.name);\n        if (values.length > 0 && !checkTypeMatch(column.name, column.type, values)) {\n          errors.push(`Column '${column.name}' type '${column.type}' does not match actual data types`);\n        }\n      }\n    }\n  }\n}\n\n// Validation 3: Check FK relationships have >70% overlapping keys\nif (schema.tables && Array.isArray(schema.tables)) {\n  for (const table of schema.tables) {\n    if (table.foreignKeys && Array.isArray(table.foreignKeys)) {\n      for (const fk of table.foreignKeys) {\n        const fkValues = getColumnValues(fk.column);\n        const pkValues = getColumnValues(fk.references.column);\n        \n        if (fkValues.length > 0 && pkValues.length > 0) {\n          const pkSet = new Set(pkValues);\n          const matchingCount = fkValues.filter(v => pkSet.has(v)).length;\n          const overlapRatio = matchingCount / fkValues.length;\n          \n          if (overlapRatio < minForeignKeyOverlap) {\n            errors.push(`Foreign key '${fk.column}' -> '${fk.references.table}.${fk.references.column}' has only ${(overlapRatio * 100).toFixed(1)}% overlap (minimum ${(minForeignKeyOverlap * 100)}% required)`);\n          }\n        }\n      }\n    }\n  }\n}\n\n// Validation 4: Check for cycle relationships\nif (schema.tables && Array.isArray(schema.tables)) {\n  const buildGraph = () => {\n    const graph = {};\n    for (const table of schema.tables) {\n      graph[table.name] = [];\n      if (table.foreignKeys && Array.isArray(table.foreignKeys)) {\n        for (const fk of table.foreignKeys) {\n          graph[table.name].push(fk.references.table);\n        }\n      }\n    }\n    return graph;\n  };\n  \n  const detectCycle = (graph) => {\n    const visited = new Set();\n    const recStack = new Set();\n    const path = [];\n    \n    const dfs = (node) => {\n      visited.add(node);\n      recStack.add(node);\n      path.push(node);\n      \n      if (graph[node]) {\n        for (const neighbor of graph[node]) {\n          if (!visited.has(neighbor)) {\n            if (dfs(neighbor)) return true;\n          } else if (recStack.has(neighbor)) {\n            path.push(neighbor);\n            return true;\n          }\n        }\n      }\n      \n      path.pop();\n      recStack.delete(node);\n      return false;\n    };\n    \n    for (const node in graph) {\n      if (!visited.has(node)) {\n        if (dfs(node)) {\n          return path;\n        }\n      }\n    }\n    return null;\n  };\n  \n  const graph = buildGraph();\n  const cycle = detectCycle(graph);\n  \n  if (cycle) {\n    errors.push(`Circular foreign key relationship detected: ${cycle.join(' -> ')}`);\n  }\n}\n\n// Validation 5: Cannot choose PK for columns with duplicates\nif (schema.tables && Array.isArray(schema.tables)) {\n  for (const table of schema.tables) {\n    if (table.columns && Array.isArray(table.columns)) {\n      for (const column of table.columns) {\n        if (column.primaryKey) {\n          const values = getColumnValues(column.name);\n          if (values.length > 0 && hasDuplicates(values)) {\n            errors.push(`Column '${column.name}' is marked as primary key but contains duplicate values`);\n          }\n        }\n      }\n    }\n  }\n}\n\n// Validation 6: Decimal precision inferred from data\nif (schema.tables && Array.isArray(schema.tables)) {\n  for (const table of schema.tables) {\n    if (table.columns && Array.isArray(table.columns)) {\n      for (const column of table.columns) {\n        const baseType = column.type.split('(')[0].toUpperCase();\n        if (baseType === 'DECIMAL' || baseType === 'NUMERIC') {\n          const values = getColumnValues(column.name);\n          if (values.length > 0) {\n            const inferred = inferDecimalPrecision(values);\n            const declaredMatch = column.type.match(/\\((\\d+),\\s*(\\d+)\\)/);\n            \n            if (declaredMatch) {\n              const declaredPrecision = parseInt(declaredMatch[1]);\n              const declaredScale = parseInt(declaredMatch[2]);\n              \n              if (inferred.precision > declaredPrecision || inferred.scale > declaredScale) {\n                warnings.push(`Column '${column.name}' declared as ${column.type} but data requires DECIMAL(${inferred.precision}, ${inferred.scale})`);\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\n// Validation 7: Check constraints match value ranges\nif (schema.tables && Array.isArray(schema.tables)) {\n  for (const table of schema.tables) {\n    if (table.columns && Array.isArray(table.columns)) {\n      for (const column of table.columns) {\n        if (column.constraints && Array.isArray(column.constraints)) {\n          const values = getColumnValues(column.name);\n          const range = getValueRange(values);\n          \n          for (const constraint of column.constraints) {\n            if (constraint.includes('>') || constraint.includes('<') || constraint.includes('BETWEEN')) {\n              if (range) {\n                // Extract numeric values from constraint\n                const numbers = constraint.match(/\\d+(\\.\\d+)?/g);\n                if (numbers) {\n                  const constraintValues = numbers.map(n => Number(n));\n                  \n                  if (constraint.includes('>') && !constraint.includes('>=')) {\n                    if (range.min <= constraintValues[0]) {\n                      warnings.push(`Column '${column.name}' has constraint '${constraint}' but data contains values <= ${constraintValues[0]}`);\n                    }\n                  } else if (constraint.includes('>=')) {\n                    if (range.min < constraintValues[0]) {\n                      warnings.push(`Column '${column.name}' has constraint '${constraint}' but data contains values < ${constraintValues[0]}`);\n                    }\n                  }\n                  \n                  if (constraint.includes('<') && !constraint.includes('<=')) {\n                    if (range.max >= constraintValues[constraintValues.length - 1]) {\n                      warnings.push(`Column '${column.name}' has constraint '${constraint}' but data contains values >= ${constraintValues[constraintValues.length - 1]}`);\n                    }\n                  } else if (constraint.includes('<=')) {\n                    if (range.max > constraintValues[constraintValues.length - 1]) {\n                      warnings.push(`Column '${column.name}' has constraint '${constraint}' but data contains values > ${constraintValues[constraintValues.length - 1]}`);\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\n// Determine if validation passed\nconst isValid = errors.length === 0;\n\n// Return validation result\nreturn [{\n  json: {\n    ...schemaData,\n    validation: {\n      isValid,\n      errors,\n      warnings,\n      timestamp: new Date().toISOString()\n    }\n  }\n}];"},"typeVersion":2},{"id":"629a370a-55e4-4375-b5e2-034f4ee0d722","name":"Check Validation Result","type":"n8n-nodes-base.if","position":[-160,64],"parameters":{"options":{},"conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"id-1","operator":{"type":"boolean","operation":"true"},"leftValue":"={{ $json.validationResult.isValid }}"}]}},"typeVersion":2.3},{"id":"95cf0cff-2f8c-4862-bef9-1bf490168cfd","name":"Prepare Revision Feedback","type":"n8n-nodes-base.set","position":[-352,592],"parameters":{"options":{},"assignments":{"assignments":[{"id":"id-1","name":"revisionFeedback","type":"string","value":"={{ 'Validation Errors: ' + JSON.stringify($('Rules Validation Layer').item.json.errors) + ' | Warnings: ' + JSON.stringify($('Rules Validation Layer').item.json.warnings) }}"},{"id":"id-2","name":"attemptNumber","type":"number","value":"={{ ($json.attemptNumber || 0) + 1 }}"},{"id":"id-3","name":"originalData","type":"object","value":"={{ JSON.stringify({ columnStats: $('Column Profiling Engine').item.json.columnStats, sampleRows: $('Column Profiling Engine').item.json.sampleRows }) }}"}]},"includeOtherFields":true},"typeVersion":3.4},{"id":"c017017b-6acd-45a3-bdb5-3f7fe5f391db","name":"Generate SQL DDL","type":"n8n-nodes-base.code","position":[128,112],"parameters":{"jsCode":"// Generate Production-Grade SQL DDL from Validated Schema\n// This code creates comprehensive CREATE TABLE statements with all constraints\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const schema = item.json.validatedSchema || item.json.schema || item.json;\n  const tables = schema.tables || [];\n  const relationships = schema.relationships || [];\n  const metadata = schema.metadata || {};\n  \n  let ddl = [];\n  \n  // Add header comment\n  ddl.push('-- ========================================');\n  ddl.push('-- Production Database Schema DDL');\n  ddl.push(`-- Generated: ${new Date().toISOString()}`);\n  if (metadata.fileName) {\n    ddl.push(`-- Source File: ${metadata.fileName}`);\n  }\n  ddl.push('-- ========================================\\n');\n  \n  // Generate CREATE TABLE statements\n  for (const table of tables) {\n    ddl.push(`-- Table: ${table.name}`);\n    if (table.description) {\n      ddl.push(`-- Description: ${table.description}`);\n    }\n    ddl.push(`CREATE TABLE ${table.name} (`);\n    \n    const columnDefs = [];\n    const constraints = [];\n    \n    // Process columns\n    for (let i = 0; i < table.columns.length; i++) {\n      const col = table.columns[i];\n      let colDef = `    ${col.name} ${col.dataType}`;\n      \n      // Add length/precision if specified\n      if (col.length) {\n        colDef += `(${col.length})`;\n      } else if (col.precision && col.scale) {\n        colDef += `(${col.precision}, ${col.scale})`;\n      }\n      \n      // Add NOT NULL constraint\n      if (col.nullable === false || col.required === true) {\n        colDef += ' NOT NULL';\n      }\n      \n      // Add DEFAULT value\n      if (col.defaultValue !== undefined && col.defaultValue !== null) {\n        if (typeof col.defaultValue === 'string') {\n          colDef += ` DEFAULT '${col.defaultValue}'`;\n        } else {\n          colDef += ` DEFAULT ${col.defaultValue}`;\n        }\n      }\n      \n      // Add CHECK constraints\n      if (col.checkConstraint) {\n        colDef += ` CHECK (${col.checkConstraint})`;\n      }\n      \n      // Add column comment\n      if (col.description) {\n        colDef += ` -- ${col.description}`;\n      }\n      \n      columnDefs.push(colDef);\n    }\n    \n    // Add PRIMARY KEY constraint\n    const pkColumns = table.columns.filter(c => c.isPrimaryKey || c.primaryKey);\n    if (pkColumns.length > 0) {\n      const pkNames = pkColumns.map(c => c.name).join(', ');\n      constraints.push(`    PRIMARY KEY (${pkNames})`);\n    }\n    \n    // Add UNIQUE constraints\n    const uniqueColumns = table.columns.filter(c => c.isUnique || c.unique);\n    for (const col of uniqueColumns) {\n      constraints.push(`    UNIQUE (${col.name})`);\n    }\n    \n    // Combine columns and constraints\n    const allDefs = [...columnDefs, ...constraints];\n    ddl.push(allDefs.join(',\\n'));\n    ddl.push(');\\n');\n    \n    // Add table indexes\n    const indexedColumns = table.columns.filter(c => c.indexed || c.index);\n    for (const col of indexedColumns) {\n      if (!col.isPrimaryKey && !col.isUnique) {\n        ddl.push(`CREATE INDEX idx_${table.name}_${col.name} ON ${table.name}(${col.name});`);\n      }\n    }\n    \n    if (indexedColumns.length > 0) {\n      ddl.push('');\n    }\n  }\n  \n  // Generate FOREIGN KEY constraints\n  if (relationships && relationships.length > 0) {\n    ddl.push('-- Foreign Key Constraints');\n    ddl.push('-- Establishing referential integrity between tables\\n');\n    \n    for (const rel of relationships) {\n      const fkName = `fk_${rel.fromTable}_${rel.toTable}`;\n      ddl.push(`ALTER TABLE ${rel.fromTable}`);\n      ddl.push(`    ADD CONSTRAINT ${fkName}`);\n      ddl.push(`    FOREIGN KEY (${rel.fromColumn})`);\n      ddl.push(`    REFERENCES ${rel.toTable}(${rel.toColumn})`);\n      \n      // Add ON DELETE and ON UPDATE actions\n      const onDelete = rel.onDelete || 'NO ACTION';\n      const onUpdate = rel.onUpdate || 'NO ACTION';\n      ddl.push(`    ON DELETE ${onDelete}`);\n      ddl.push(`    ON UPDATE ${onUpdate};\\n`);\n    }\n  }\n  \n  // Add design decision comments\n  ddl.push('-- ========================================');\n  ddl.push('-- Design Decisions and Notes');\n  ddl.push('-- ========================================');\n  ddl.push('-- 1. All tables include appropriate primary keys for data integrity');\n  ddl.push('-- 2. Foreign keys enforce referential integrity between related tables');\n  ddl.push('-- 3. NOT NULL constraints prevent missing critical data');\n  ddl.push('-- 4. Indexes created on frequently queried columns for performance');\n  ddl.push('-- 5. Data types selected based on column profiling and business rules');\n  if (metadata.designNotes) {\n    ddl.push(`-- 6. Additional Notes: ${metadata.designNotes}`);\n  }\n  \n  const sqlDDL = ddl.join('\\n');\n  \n  results.push({\n    json: {\n      ...item.json,\n      sqlDDL: sqlDDL,\n      ddlGenerated: true,\n      generatedAt: new Date().toISOString(),\n      tableCount: tables.length,\n      relationshipCount: relationships.length\n    }\n  });\n}\n\nreturn results;"},"typeVersion":2},{"id":"400d66c3-f4e1-45e0-bec9-640d20726fe5","name":"Generate ERD & Data Dictionary","type":"n8n-nodes-base.code","position":[128,304],"parameters":{"jsCode":"// Generate ERD & Data Dictionary from validated schema\nconst input = $input.all();\nconst schemaData = input[0].json;\n\n// Extract schema and profiling data\nconst schema = schemaData.schema || schemaData;\nconst tables = schema.tables || [];\nconst relationships = schema.relationships || [];\nconst profilingData = schemaData.profilingData || {};\n\n// Generate ERD Summary\nconst erdSummary = {\n  totalTables: tables.length,\n  totalRelationships: relationships.length,\n  tables: tables.map(table => ({\n    name: table.name,\n    description: table.description || '',\n    columnCount: table.columns ? table.columns.length : 0,\n    primaryKey: table.columns ? table.columns.find(col => col.isPrimaryKey)?.name : null\n  })),\n  relationships: relationships.map(rel => {\n    // Determine cardinality\n    let cardinality = '1:N'; // Default\n    if (rel.cardinality) {\n      cardinality = rel.cardinality;\n    } else if (rel.type === 'one-to-one') {\n      cardinality = '1:1';\n    } else if (rel.type === 'many-to-many') {\n      cardinality = 'N:M';\n    } else if (rel.type === 'one-to-many') {\n      cardinality = '1:N';\n    }\n    \n    return {\n      fromTable: rel.fromTable || rel.sourceTable,\n      toTable: rel.toTable || rel.targetTable,\n      fromColumn: rel.fromColumn || rel.sourceColumn,\n      toColumn: rel.toColumn || rel.targetColumn,\n      cardinality: cardinality,\n      relationshipType: rel.type || 'foreign-key',\n      description: rel.description || `${rel.fromTable} references ${rel.toTable}`\n    };\n  })\n};\n\n// Generate Complete Data Dictionary\nconst dataDictionary = [];\n\nfor (const table of tables) {\n  if (!table.columns) continue;\n  \n  for (const column of table.columns) {\n    // Get sample values from profiling data if available\n    let sampleValues = [];\n    if (profilingData[table.name] && profilingData[table.name][column.name]) {\n      const colProfile = profilingData[table.name][column.name];\n      sampleValues = colProfile.sampleValues || colProfile.uniqueValues || [];\n      // Limit to 5 samples\n      if (sampleValues.length > 5) {\n        sampleValues = sampleValues.slice(0, 5);\n      }\n    }\n    \n    // Build constraints list\n    const constraints = [];\n    if (column.isPrimaryKey) constraints.push('PRIMARY KEY');\n    if (column.isForeignKey) constraints.push('FOREIGN KEY');\n    if (column.isUnique) constraints.push('UNIQUE');\n    if (column.isNotNull || column.required) constraints.push('NOT NULL');\n    if (column.defaultValue) constraints.push(`DEFAULT: ${column.defaultValue}`);\n    if (column.maxLength) constraints.push(`MAX LENGTH: ${column.maxLength}`);\n    if (column.minValue !== undefined) constraints.push(`MIN: ${column.minValue}`);\n    if (column.maxValue !== undefined) constraints.push(`MAX: ${column.maxValue}`);\n    \n    dataDictionary.push({\n      tableName: table.name,\n      columnName: column.name,\n      dataType: column.type || column.dataType,\n      description: column.description || '',\n      constraints: constraints.join(', '),\n      nullable: column.isNotNull ? 'NO' : 'YES',\n      sampleValues: sampleValues,\n      businessMeaning: column.businessMeaning || column.description || ''\n    });\n  }\n}\n\n// Generate Mermaid ERD diagram code\nlet mermaidERD = 'erDiagram\\n';\n\n// Add tables with their columns\nfor (const table of tables) {\n  mermaidERD += `  ${table.name} {\\n`;\n  if (table.columns) {\n    for (const column of table.columns) {\n      const type = column.type || column.dataType || 'string';\n      let keyIndicator = '';\n      if (column.isPrimaryKey) keyIndicator = ' PK';\n      else if (column.isForeignKey) keyIndicator = ' FK';\n      mermaidERD += `    ${type} ${column.name}${keyIndicator}\\n`;\n    }\n  }\n  mermaidERD += `  }\\n`;\n}\n\n// Add relationships\nfor (const rel of relationships) {\n  const fromTable = rel.fromTable || rel.sourceTable;\n  const toTable = rel.toTable || rel.targetTable;\n  let relationshipSymbol = '||--o{';\n  \n  // Map cardinality to Mermaid symbols\n  if (rel.cardinality === '1:1') {\n    relationshipSymbol = '||--||';\n  } else if (rel.cardinality === 'N:M') {\n    relationshipSymbol = '}o--o{';\n  } else if (rel.cardinality === '1:N') {\n    relationshipSymbol = '||--o{';\n  }\n  \n  const label = rel.description || 'references';\n  mermaidERD += `  ${fromTable} ${relationshipSymbol} ${toTable} : \"${label}\"\\n`;\n}\n\n// Return comprehensive ERD and data dictionary\nreturn [{\n  json: {\n    erdSummary: erdSummary,\n    dataDictionary: dataDictionary,\n    mermaidERD: mermaidERD,\n    metadata: {\n      generatedAt: new Date().toISOString(),\n      totalTables: tables.length,\n      totalColumns: dataDictionary.length,\n      totalRelationships: relationships.length\n    }\n  }\n}];"},"typeVersion":2},{"id":"c3b15664-431b-4894-8e1e-a57f31574f05","name":"Generate Load Plan","type":"n8n-nodes-base.code","position":[112,576],"parameters":{"jsCode":"// Generate Load Plan with dependency analysis and insertion order\n\nconst items = $input.all();\nconst schemaData = items[0].json;\n\n// Extract tables and relationships from schema\nconst tables = schemaData.schema?.tables || [];\nconst relationships = schemaData.schema?.relationships || [];\n\n// Build dependency graph\nfunction buildDependencyGraph(tables, relationships) {\n  const graph = {};\n  const inDegree = {};\n  \n  // Initialize graph with all tables\n  tables.forEach(table => {\n    graph[table.name] = [];\n    inDegree[table.name] = 0;\n  });\n  \n  // Build edges based on foreign key relationships\n  relationships.forEach(rel => {\n    if (rel.type === 'foreign_key') {\n      // Child table depends on parent table\n      const childTable = rel.from_table;\n      const parentTable = rel.to_table;\n      \n      if (graph[parentTable] && !graph[parentTable].includes(childTable)) {\n        graph[parentTable].push(childTable);\n        inDegree[childTable]++;\n      }\n    }\n  });\n  \n  return { graph, inDegree };\n}\n\n// Topological sort to determine load order\nfunction topologicalSort(graph, inDegree) {\n  const queue = [];\n  const result = [];\n  const visited = new Set();\n  \n  // Find all nodes with no dependencies\n  Object.keys(inDegree).forEach(table => {\n    if (inDegree[table] === 0) {\n      queue.push(table);\n    }\n  });\n  \n  while (queue.length > 0) {\n    const current = queue.shift();\n    result.push(current);\n    visited.add(current);\n    \n    // Process dependent tables\n    (graph[current] || []).forEach(dependent => {\n      inDegree[dependent]--;\n      if (inDegree[dependent] === 0) {\n        queue.push(dependent);\n      }\n    });\n  }\n  \n  return result;\n}\n\n// Detect circular dependencies\nfunction detectCircularDependencies(tables, sortedOrder) {\n  const circular = [];\n  tables.forEach(table => {\n    if (!sortedOrder.includes(table.name)) {\n      circular.push(table.name);\n    }\n  });\n  return circular;\n}\n\n// Generate batch recommendations\nfunction generateBatchRecommendations(sortedOrder, tables) {\n  const batches = [];\n  let currentBatch = [];\n  \n  sortedOrder.forEach((tableName, index) => {\n    const table = tables.find(t => t.name === tableName);\n    currentBatch.push(tableName);\n    \n    // Create batches of 5 tables or at natural dependency breaks\n    if (currentBatch.length >= 5 || index === sortedOrder.length - 1) {\n      batches.push([...currentBatch]);\n      currentBatch = [];\n    }\n  });\n  \n  return batches;\n}\n\n// Generate Mermaid diagram for dependency visualization\nfunction generateDependencyDiagram(graph, sortedOrder) {\n  let mermaid = 'graph TD\\n';\n  \n  sortedOrder.forEach(table => {\n    const dependencies = graph[table] || [];\n    if (dependencies.length === 0) {\n      mermaid += `  ${table}[${table}]\\n`;\n    } else {\n      dependencies.forEach(dep => {\n        mermaid += `  ${table}[${table}] --> ${dep}[${dep}]\\n`;\n      });\n    }\n  });\n  \n  return mermaid;\n}\n\n// Build the dependency graph\nconst { graph, inDegree } = buildDependencyGraph(tables, relationships);\n\n// Get topological sort order\nconst loadOrder = topologicalSort(graph, JSON.parse(JSON.stringify(inDegree)));\n\n// Detect circular dependencies\nconst circularDeps = detectCircularDependencies(tables, loadOrder);\n\n// Generate batch recommendations\nconst batches = generateBatchRecommendations(loadOrder, tables);\n\n// Generate dependency diagram\nconst dependencyDiagram = generateDependencyDiagram(graph, loadOrder);\n\n// Create detailed load plan\nconst loadPlan = {\n  load_order: loadOrder,\n  total_tables: tables.length,\n  batches: batches.map((batch, index) => ({\n    batch_number: index + 1,\n    tables: batch,\n    description: `Load batch ${index + 1} - ${batch.length} table(s)`\n  })),\n  circular_dependencies: circularDeps.length > 0 ? {\n    detected: true,\n    tables: circularDeps,\n    resolution_strategy: \"Disable foreign key constraints temporarily, load data, then re-enable constraints\"\n  } : {\n    detected: false,\n    message: \"No circular dependencies detected\"\n  },\n  dependency_graph: dependencyDiagram,\n  recommendations: [\n    \"Load tables in the specified order to satisfy foreign key constraints\",\n    \"Use batch processing for large datasets (recommended batch size: 1000-5000 rows)\",\n    \"Disable indexes before bulk load and rebuild after completion for better performance\",\n    \"Use transactions for each batch to ensure data consistency\",\n    circularDeps.length > 0 ? \"Handle circular dependencies by temporarily disabling FK constraints\" : \"No circular dependencies to handle\",\n    \"Validate data integrity after load completion\",\n    \"Consider parallel loading for independent tables (tables with no dependencies)\"\n  ],\n  insertion_strategy: {\n    independent_tables: loadOrder.filter(table => inDegree[table] === 0),\n    dependent_tables: loadOrder.filter(table => inDegree[table] > 0),\n    parallel_load_possible: loadOrder.filter(table => inDegree[table] === 0).length > 1\n  }\n};\n\n// Return the load plan\nreturn [{\n  json: {\n    ...schemaData,\n    load_plan: loadPlan\n  }\n}];"},"typeVersion":2},{"id":"0675b44d-f19a-402d-92bb-efdcb4554248","name":"Combine Final Outputs","type":"n8n-nodes-base.merge","position":[400,192],"parameters":{"mode":"combine","options":{},"combineBy":"combineAll"},"typeVersion":3.2},{"id":"73a2eec7-43a2-4ee1-bf31-13798a6d49ce","name":"Explanation Agent (Optional)","type":"@n8n/n8n-nodes-langchain.agent","position":[560,192],"parameters":{"text":"={{ $json.userQuestion || \"Provide a summary of the generated schema and key design decisions.\" }}","options":{"systemMessage":"You are a database schema expert providing explanations to clients.\n\nYou have access to:\n- The complete generated schema (SQL DDL, ERD, data dictionary, load plan)\n- Original data profiling statistics\n- Design decisions and reasoning\n\nAnswer questions like:\n- \"Why did you choose customer_id as FK?\"\n- \"Which columns were merged and why?\"\n- \"What are possible issues in this schema?\"\n- \"Suggest index improvements\"\n- \"Explain the normalization decisions\"\n\nProvide clear, professional explanations that justify design choices with data evidence."},"promptType":"define"},"typeVersion":3},{"id":"2ecc3f41-3280-4e31-91e6-093e23cada12","name":"Return Schema Results","type":"n8n-nodes-base.respondToWebhook","position":[1056,192],"parameters":{"options":{},"respondWith":"json","responseBody":"={\n  \"success\": true,\n  \"schema\": {\n    \"sqlDDL\": {{ $json.sqlDDL }},\n    \"erdSummary\": {{ $json.erdSummary }},\n    \"dataDictionary\": {{ $json.dataDictionary }},\n    \"loadPlan\": {{ $json.loadPlan }},\n    \"aiExplanation\": {{ $json.explanation || null }}\n  },\n  \"metadata\": {\n    \"generatedAt\": {{ new Date().toISOString() }},\n    \"fileHash\": {{ $json.fileHash || null }}\n  }\n}"},"typeVersion":1.5},{"id":"7f02b7c5-1bc0-47bf-8379-17f4a516cf8a","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[-3040,-144],"parameters":{"width":384,"height":480,"content":"## AI Data Schema Generator\n\n## How it works\nThis workflow accepts CSV/XLSX files via webhook, extracts and cleans the data, and performs deep profiling to understand column types, relationships, and data quality. An AI agent then generates a normalized database schema, which is validated against strict rules before producing SQL DDL, ERD, data dictionary, and load plan outputs.\n\n## Setup steps\n\nActivate the webhook and send a CSV/XLSX file\nConfigure AI (OpenAI / LangChain credentials)\nAdjust thresholds (confidence, FK overlap) if needed\nReview generated schema, SQL, and ERD outputs"},"typeVersion":1},{"id":"88092068-2f50-4df0-9a54-f5c4fd94f5a1","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[-2576,-128],"parameters":{"color":7,"width":550,"height":400,"content":"## Input & Config\n\nHandles file upload and workflow settings.\n- Webhook receives file  \n- Config defines thresholds  \n- Prepares data for processing  "},"typeVersion":1},{"id":"4f1b9743-e9a1-4338-9255-4620127879a3","name":"Sticky Note3","type":"n8n-nodes-base.stickyNote","position":[-1504,-144],"parameters":{"color":7,"width":246,"height":480,"content":"## merge\n extracted data is merged\n"},"typeVersion":1},{"id":"2e531cd3-96b8-415b-a9bc-775328463885","name":"Sticky Note5","type":"n8n-nodes-base.stickyNote","position":[368,-16],"parameters":{"color":7,"width":486,"height":656,"content":"## Combine & Explain\n\nMerges outputs and adds insights.\n\n- Combines DDL, ERD, load plan  \n- Optional AI explanation  \n- Uses LLM for schema reasoning  "},"typeVersion":1},{"id":"ed3a0e20-bfcd-4c9b-94d9-625087fcadaf","name":"Sticky Note6","type":"n8n-nodes-base.stickyNote","position":[-2000,-144],"parameters":{"color":7,"width":486,"height":480,"content":"## File Extraction\n\nExtracts structured data from uploaded files.\n\n- Detects Excel vs CSV  \n- Parses rows into JSON  \n- Merges extracted outputs  "},"typeVersion":1},{"id":"76147c0f-c496-4748-aef0-e6ca54bfa292","name":"Sticky Note7","type":"n8n-nodes-base.stickyNote","position":[-1232,-144],"parameters":{"color":7,"width":358,"height":480,"content":"## Column Analysis Engine\n\nPerforms deep column-level profiling.\n- Detects types, nulls, uniqueness  \n- Identifies potential IDs  \n- Suggests foreign key candidates  "},"typeVersion":1},{"id":"aa68b698-0219-4ba8-bde1-3397e16b46b3","name":"OpenAI GPT","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[-816,240],"parameters":{"model":{"__rl":true,"mode":"id","value":"gpt-4o"},"options":{},"builtInTools":{}},"typeVersion":1.3},{"id":"db050ca1-3c69-4bb6-a960-c0655fb35067","name":"Sticky Note8","type":"n8n-nodes-base.stickyNote","position":[-464,-160],"parameters":{"color":7,"width":470,"height":384,"content":"## Validation Layer\n\nEnsures schema correctness.\n\n- Checks type accuracy  \n- Validates PK/FK rules  \n- Detects conflicts and errors  "},"typeVersion":1},{"id":"3036509c-933d-43fe-af21-288c1a7b4ef6","name":"Sticky Note9","type":"n8n-nodes-base.stickyNote","position":[-848,-144],"parameters":{"color":7,"width":358,"height":496,"content":"## AI Schema Generation\n\nAI designs database schema.\n- Normalizes table structure  \n- Assigns SQL types  \n- Defines PKs, FKs, constraints  "},"typeVersion":1},{"id":"7b046947-b4b2-4dff-871a-c212d11ffafe","name":"Sticky Note11","type":"n8n-nodes-base.stickyNote","position":[-560,432],"parameters":{"color":7,"width":438,"height":352,"content":"## Revision Handling\n\nImproves schema if validation fails.\n\n- Sends feedback to AI  \n- Tracks retry attempts  "},"typeVersion":1},{"id":"ae85bd8f-eea9-4c54-80ee-28e624cc3744","name":"Sticky Note12","type":"n8n-nodes-base.stickyNote","position":[32,-144],"parameters":{"color":7,"width":294,"height":608,"content":"## Schema Output Generation\n\nGenerates core database outputs.\n\n- SQL DDL creation  \n- ERD + data dictionary  \n- Load plan generation  "},"typeVersion":1},{"id":"3bd311bf-f9a9-4d5d-b728-8ebfa7b65906","name":"Sticky Note13","type":"n8n-nodes-base.stickyNote","position":[896,0],"parameters":{"color":7,"width":358,"height":416,"content":"## Response Output\n\nReturns final structured result.\n\n- Sends JSON response via webhook  \n- Includes all generated artifacts  \n- Adds metadata and timestamps  "},"typeVersion":1},{"id":"2191a217-3296-4f40-b159-2e35970d8942","name":"Sticky Note14","type":"n8n-nodes-base.stickyNote","position":[-32,480],"parameters":{"color":7,"width":374,"height":256,"content":"## Load Plan Engine\n\n- Calculates load sequence  "},"typeVersion":1},{"id":"8a8bb5cd-7ebb-4672-80a2-7a028c676f0c","name":"OpenAI GPT-(Explanation)","type":"@n8n/n8n-nodes-langchain.lmChatOpenAi","position":[608,496],"parameters":{"model":{"__rl":true,"mode":"id","value":"gpt-4o"},"options":{},"builtInTools":{}},"typeVersion":1.3}],"pinData":{},"connections":{"OpenAI GPT":{"ai_languageModel":[[{"node":"Schema Reasoning Agent","type":"ai_languageModel","index":0}]]},"Check File Type":{"main":[[{"node":"Extract Excel Data","type":"main","index":0}],[{"node":"Extract CSV Data","type":"main","index":0}]]},"Extract CSV Data":{"main":[[{"node":"Merge Extracted Data","type":"main","index":1}]]},"Generate SQL DDL":{"main":[[{"node":"Combine Final Outputs","type":"main","index":0}]]},"Extract Excel Data":{"main":[[{"node":"Merge Extracted Data","type":"main","index":0}]]},"Generate Load Plan":{"main":[[{"node":"Return Schema Results","type":"main","index":0}]]},"File Upload Webhook":{"main":[[{"node":"Workflow Configuration","type":"main","index":0}]]},"Merge Extracted Data":{"main":[[{"node":"Compute File Hash & Profile Data","type":"main","index":0}]]},"Schema Output Parser":{"ai_outputParser":[[{"node":"Schema Reasoning Agent","type":"ai_outputParser","index":0}]]},"Combine Final Outputs":{"main":[[{"node":"Explanation Agent (Optional)","type":"main","index":0}]]},"Rules Validation Layer":{"main":[[{"node":"Check Validation Result","type":"main","index":0}]]},"Schema Reasoning Agent":{"main":[[{"node":"Rules Validation Layer","type":"main","index":0}]]},"Workflow Configuration":{"main":[[{"node":"Check File Type","type":"main","index":0}]]},"Check Validation Result":{"main":[[{"node":"Generate SQL DDL","type":"main","index":0},{"node":"Generate ERD & Data Dictionary","type":"main","index":0},{"node":"Generate Load Plan","type":"main","index":0}],[{"node":"Prepare Revision Feedback","type":"main","index":0}]]},"Column Profiling Engine":{"main":[[{"node":"Schema Reasoning Agent","type":"main","index":0}]]},"OpenAI GPT-(Explanation)":{"ai_languageModel":[[{"node":"Explanation Agent (Optional)","type":"ai_languageModel","index":0}]]},"Prepare Revision Feedback":{"main":[[{"node":"Schema Reasoning Agent","type":"main","index":0}]]},"Explanation Agent (Optional)":{"main":[[{"node":"Return Schema Results","type":"main","index":0}]]},"Generate ERD & Data Dictionary":{"main":[[{"node":"Combine Final Outputs","type":"main","index":1}]]},"Compute File Hash & Profile Data":{"main":[[{"node":"Column Profiling Engine","type":"main","index":0}]]}}},"lastUpdatedBy":1,"workflowInfo":{"nodeCount":33,"nodeTypes":{"n8n-nodes-base.if":{"count":2},"n8n-nodes-base.set":{"count":2},"n8n-nodes-base.code":{"count":6},"n8n-nodes-base.merge":{"count":2},"n8n-nodes-base.webhook":{"count":1},"n8n-nodes-base.stickyNote":{"count":12},"@n8n/n8n-nodes-langchain.agent":{"count":2},"n8n-nodes-base.extractFromFile":{"count":2},"n8n-nodes-base.respondToWebhook":{"count":1},"@n8n/n8n-nodes-langchain.lmChatOpenAi":{"count":2},"@n8n/n8n-nodes-langchain.outputParserStructured":{"count":1}}},"status":"published","readyToDemo":null,"user":{"name":"ResilNext","username":"rnair1996","bio":"","verified":true,"links":[""],"avatar":"https://gravatar.com/avatar/c20bc6c3bcdf260fac3c28c556a8db661ee93670037a3ceb857e047851f6f438?r=pg&d=retro&size=200"},"nodes":[{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","codex":{"data":{"alias":["Router","Filter","Condition","Logic","Boolean","Branch"],"details":"The IF node can be used to implement binary conditional logic in your workflow. You can set up one-to-many conditions to evaluate each item of data being inputted into the node. That data will either evaluate to TRUE or FALSE and route out of the node accordingly.\n\nThis node has multiple types of conditions: Bool, String, Number, and Date & Time.","resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/","icon":"🧬","label":"Why business process automation with n8n can change your daily life"},{"url":"https://n8n.io/blog/create-a-toxic-language-detector-for-telegram/","icon":"🤬","label":"Create a toxic language detector for Telegram in 4 step"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/","icon":"🏷️","label":"How to automatically manage contributions to open-source projects"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.if/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow"]}}},"group":"[\"transform\"]","defaults":{"name":"If","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"displayName":"If","typeVersion":2,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":24,"icon":"file:merge.svg","name":"n8n-nodes-base.merge","codex":{"data":{"alias":["Join","Concatenate","Wait"],"resources":{"generic":[{"url":"https://n8n.io/blog/how-to-sync-data-between-two-systems/","icon":"🏬","label":"How to synchronize data between two systems (one-way vs. two-way sync"},{"url":"https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/","icon":"🎫","label":"Supercharging your conference registration process with n8n"},{"url":"https://n8n.io/blog/migrating-community-metrics-to-orbit-using-n8n/","icon":"📈","label":"Migrating Community Metrics to Orbit using n8n"},{"url":"https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/","icon":"👦","label":"Build your own virtual assistant with n8n: A step by step guide"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.merge/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Merge"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTc3XzUxOCkiPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgNDhDMCAyMS40OTAzIDIxLjQ5MDMgMCA0OCAwSDExMkMxMzguNTEgMCAxNjAgMjEuNDkwMyAxNjAgNDhWNTZIMTk2LjI1MkMyNDAuNDM1IDU2IDI3Ni4yNTIgOTEuODE3MiAyNzYuMjUyIDEzNlYxOTJDMjc2LjI1MiAyMTQuMDkxIDI5NC4xNjEgMjMyIDMxNi4yNTIgMjMySDM1MlYyMjRDMzUyIDE5Ny40OSAzNzMuNDkgMTc2IDQwMCAxNzZINDY0QzQ5MC41MSAxNzYgNTEyIDE5Ny40OSA1MTIgMjI0VjI4OEM1MTIgMzE0LjUxIDQ5MC41MSAzMzYgNDY0IDMzNkg0MDBDMzczLjQ5IDMzNiAzNTIgMzE0LjUxIDM1MiAyODhWMjgwSDMxNi4yNTJDMjk0LjE2MSAyODAgMjc2LjI1MiAyOTcuOTA5IDI3Ni4yNTIgMzIwVjM3NkMyNzYuMjUyIDQyMC4xODMgMjQwLjQzNSA0NTYgMTk2LjI1MiA0NTZIMTYwVjQ2NEMxNjAgNDkwLjUxIDEzOC41MSA1MTIgMTEyIDUxMkg0OEMyMS40OTAzIDUxMiAwIDQ5MC41MSAwIDQ2NFY0MDBDMCAzNzMuNDkgMjEuNDkwMyAzNTIgNDggMzUySDExMkMxMzguNTEgMzUyIDE2MCAzNzMuNDkgMTYwIDQwMFY0MDhIMTk2LjI1MkMyMTMuOTI1IDQwOCAyMjguMjUyIDM5My42NzMgMjI4LjI1MiAzNzZWMzIwQzIyOC4yNTIgMjk0Ljc4NCAyMzguODU5IDI3Mi4wNDQgMjU1Ljg1MyAyNTZDMjM4Ljg1OSAyMzkuOTU2IDIyOC4yNTIgMjE3LjIxNiAyMjguMjUyIDE5MlYxMzZDMjI4LjI1MiAxMTguMzI3IDIxMy45MjUgMTA0IDE5Ni4yNTIgMTA0SDE2MFYxMTJDMTYwIDEzOC41MSAxMzguNTEgMTYwIDExMiAxNjBINDhDMjEuNDkwMyAxNjAgMCAxMzguNTEgMCAxMTJWNDhaTTEwNCA0OEMxMDguNDE4IDQ4IDExMiA1MS41ODE3IDExMiA1NlYxMDRDMTEyIDEwOC40MTggMTA4LjQxOCAxMTIgMTA0IDExMkg1NkM1MS41ODE3IDExMiA0OCAxMDguNDE4IDQ4IDEwNFY1NkM0OCA1MS41ODE3IDUxLjU4MTcgNDggNTYgNDhIMTA0Wk00NTYgMjI0QzQ2MC40MTggMjI0IDQ2NCAyMjcuNTgyIDQ2NCAyMzJWMjgwQzQ2NCAyODQuNDE4IDQ2MC40MTggMjg4IDQ1NiAyODhINDA4QzQwMy41ODIgMjg4IDQwMCAyODQuNDE4IDQwMCAyODBWMjMyQzQwMCAyMjcuNTgyIDQwMy41ODIgMjI0IDQwOCAyMjRINDU2Wk0xMTIgNDA4QzExMiA0MDMuNTgyIDEwOC40MTggNDAwIDEwNCA0MDBINTZDNTEuNTgxNyA0MDAgNDggNDAzLjU4MiA0OCA0MDhWNDU2QzQ4IDQ2MC40MTggNTEuNTgxNyA0NjQgNTYgNDY0SDEwNEMxMDguNDE4IDQ2NCAxMTIgNDYwLjQxOCAxMTIgNDU2VjQwOFoiIGZpbGw9IiM1NEI4QzkiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTc3XzUxOCI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo="},"displayName":"Merge","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":38,"icon":"fa:pen","name":"n8n-nodes-base.set","codex":{"data":{"alias":["Set","JS","JSON","Filter","Transform","Map"],"resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/automatically-pulling-and-visualizing-data-with-n8n/","icon":"📈","label":"Automatically pulling and visualizing data with n8n"},{"url":"https://n8n.io/blog/database-monitoring-and-alerting-with-n8n/","icon":"📡","label":"Database Monitoring and Alerting with n8n"},{"url":"https://n8n.io/blog/automatically-adding-expense-receipts-to-google-sheets-with-telegram-mindee-twilio-and-n8n/","icon":"🧾","label":"Automatically Adding Expense Receipts to Google Sheets with Telegram, Mindee, Twilio, and n8n"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/building-an-expense-tracking-app-in-10-minutes/","icon":"📱","label":"Building an expense tracking app in 10 minutes"},{"url":"https://n8n.io/blog/the-ultimate-guide-to-automate-your-video-collaboration-with-whereby-mattermost-and-n8n/","icon":"📹","label":"The ultimate guide to automate your video collaboration with Whereby, Mattermost, and n8n"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/","icon":"🧰","label":"Learn to Build Powerful API Endpoints Using Webhooks"},{"url":"https://n8n.io/blog/how-a-membership-development-manager-automates-his-work-and-investments/","icon":"📈","label":"How a Membership Development Manager automates his work and investments"},{"url":"https://n8n.io/blog/a-low-code-bitcoin-ticker-built-with-questdb-and-n8n-io/","icon":"📈","label":"A low-code bitcoin ticker built with QuestDB and n8n.io"},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/","icon":"🛵","label":"How Goomer automated their operations with over 200 n8n workflows"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Data Transformation"]}}},"group":"[\"input\"]","defaults":{"name":"Edit Fields"},"iconData":{"icon":"pen","type":"icon"},"displayName":"Edit Fields (Set)","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":47,"icon":"file:webhook.svg","name":"n8n-nodes-base.webhook","codex":{"data":{"alias":["HTTP","API","Build","WH"],"resources":{"generic":[{"url":"https://n8n.io/blog/learn-how-to-automatically-cross-post-your-content-with-n8n/","icon":"✍️","label":"Learn how to automatically cross-post your content with n8n"},{"url":"https://n8n.io/blog/running-n8n-on-ships-an-interview-with-maranics/","icon":"🛳","label":"Running n8n on ships: An interview with Maranics"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/what-are-apis-how-to-use-them-with-no-code/","icon":" 🪢","label":"What are APIs and how to use them with no code"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/how-a-digital-strategist-uses-n8n-for-online-marketing/","icon":"💻","label":"How a digital strategist uses n8n for online marketing"},{"url":"https://n8n.io/blog/the-ultimate-guide-to-automate-your-video-collaboration-with-whereby-mattermost-and-n8n/","icon":"📹","label":"The ultimate guide to automate your video collaboration with Whereby, Mattermost, and n8n"},{"url":"https://n8n.io/blog/how-to-automatically-give-kudos-to-contributors-with-github-slack-and-n8n/","icon":"👏","label":"How to automatically give kudos to contributors with GitHub, Slack, and n8n"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"},{"url":"https://n8n.io/blog/creating-custom-incident-response-workflows-with-n8n/","label":"How to automate every step of an incident response workflow"},{"url":"https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/","icon":"🧰","label":"Learn to Build Powerful API Endpoints Using Webhooks"},{"url":"https://n8n.io/blog/learn-how-to-use-webhooks-with-mattermost-slash-commands/","icon":"🦄","label":"Learn how to use webhooks with Mattermost slash commands"},{"url":"https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/","icon":"🛵","label":"How Goomer automated their operations with over 200 n8n workflows"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"trigger\"]","defaults":{"name":"Webhook"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCI+PHBhdGggZmlsbD0iIzM3NDc0ZiIgZD0iTTM1IDM3Yy0yLjIgMC00LTEuOC00LTRzMS44LTQgNC00IDQgMS44IDQgNC0xLjggNC00IDQiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMzUgNDNjLTMgMC01LjktMS40LTcuOC0zLjdsMy4xLTIuNWMxLjEgMS40IDIuOSAyLjMgNC43IDIuMyAzLjMgMCA2LTIuNyA2LTZzLTIuNy02LTYtNmMtMSAwLTIgLjMtMi45LjdsLTEuNyAxTDIzLjMgMTZsMy41LTEuOSA1LjMgOS40YzEtLjMgMi0uNSAzLS41IDUuNSAwIDEwIDQuNSAxMCAxMFM0MC41IDQzIDM1IDQzIi8+PHBhdGggZmlsbD0iIzM3NDc0ZiIgZD0iTTE0IDQzQzguNSA0MyA0IDM4LjUgNCAzM2MwLTQuNiAzLjEtOC41IDcuNS05LjdsMSAzLjlDOS45IDI3LjkgOCAzMC4zIDggMzNjMCAzLjMgMi43IDYgNiA2czYtMi43IDYtNnYtMmgxNXY0SDIzLjhjLS45IDQuNi01IDgtOS44IDgiLz48cGF0aCBmaWxsPSIjZTkxZTYzIiBkPSJNMTQgMzdjLTIuMiAwLTQtMS44LTQtNHMxLjgtNCA0LTQgNCAxLjggNCA0LTEuOCA0LTQgNCIvPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0yNSAxOWMtMi4yIDAtNC0xLjgtNC00czEuOC00IDQtNCA0IDEuOCA0IDQtMS44IDQtNCA0Ii8+PHBhdGggZmlsbD0iI2U5MWU2MyIgZD0ibTE1LjcgMzQtMy40LTIgNS45LTkuN2MtMi0xLjktMy4yLTQuNS0zLjItNy4zIDAtNS41IDQuNS0xMCAxMC0xMHMxMCA0LjUgMTAgMTBjMCAuOS0uMSAxLjctLjMgMi41bC0zLjktMWMuMS0uNS4yLTEgLjItMS41IDAtMy4zLTIuNy02LTYtNnMtNiAyLjctNiA2YzAgMi4xIDEuMSA0IDIuOSA1LjFsMS43IDF6Ii8+PC9zdmc+"},"displayName":"Webhook","typeVersion":2,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":535,"icon":"file:webhook.svg","name":"n8n-nodes-base.respondToWebhook","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/"}]},"categories":["Core Nodes","Utility"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"transform\"]","defaults":{"name":"Respond to Webhook"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCI+PHBhdGggZmlsbD0iIzM3NDc0ZiIgZD0iTTM1IDM3Yy0yLjIgMC00LTEuOC00LTRzMS44LTQgNC00IDQgMS44IDQgNC0xLjggNC00IDQiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMzUgNDNjLTMgMC01LjktMS40LTcuOC0zLjdsMy4xLTIuNWMxLjEgMS40IDIuOSAyLjMgNC43IDIuMyAzLjMgMCA2LTIuNyA2LTZzLTIuNy02LTYtNmMtMSAwLTIgLjMtMi45LjdsLTEuNyAxTDIzLjMgMTZsMy41LTEuOSA1LjMgOS40YzEtLjMgMi0uNSAzLS41IDUuNSAwIDEwIDQuNSAxMCAxMFM0MC41IDQzIDM1IDQzIi8+PHBhdGggZmlsbD0iIzM3NDc0ZiIgZD0iTTE0IDQzQzguNSA0MyA0IDM4LjUgNCAzM2MwLTQuNiAzLjEtOC41IDcuNS05LjdsMSAzLjlDOS45IDI3LjkgOCAzMC4zIDggMzNjMCAzLjMgMi43IDYgNiA2czYtMi43IDYtNnYtMmgxNXY0SDIzLjhjLS45IDQuNi01IDgtOS44IDgiLz48cGF0aCBmaWxsPSIjZTkxZTYzIiBkPSJNMTQgMzdjLTIuMiAwLTQtMS44LTQtNHMxLjgtNCA0LTQgNCAxLjggNCA0LTEuOCA0LTQgNCIvPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0yNSAxOWMtMi4yIDAtNC0xLjgtNC00czEuOC00IDQtNCA0IDEuOCA0IDQtMS44IDQtNCA0Ii8+PHBhdGggZmlsbD0iI2U5MWU2MyIgZD0ibTE1LjcgMzQtMy40LTIgNS45LTkuN2MtMi0xLjktMy4yLTQuNS0zLjItNy4zIDAtNS41IDQuNS0xMCAxMC0xMHMxMCA0LjUgMTAgMTBjMCAuOS0uMSAxLjctLjMgMi41bC0zLjktMWMuMS0uNS4yLTEgLjItMS41IDAtMy4zLTIuNy02LTYtNnMtNiAyLjctNiA2YzAgMi4xIDEuMSA0IDIuOSA1LjFsMS43IDF6Ii8+PC9zdmc+"},"displayName":"Respond to Webhook","typeVersion":2,"nodeCategories":[{"id":7,"name":"Utility"},{"id":9,"name":"Core Nodes"}]},{"id":565,"icon":"fa:sticky-note","name":"n8n-nodes-base.stickyNote","codex":{"data":{"alias":["Comments","Notes","Sticky"],"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"input\"]","defaults":{"name":"Sticky Note","color":"#FFD233"},"iconData":{"icon":"sticky-note","type":"icon"},"displayName":"Sticky Note","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":834,"icon":"file:code.svg","name":"n8n-nodes-base.code","codex":{"data":{"alias":["cpde","Javascript","JS","Python","Script","Custom Code","Function"],"details":"The Code node allows you to execute JavaScript in your workflow.","resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Code"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTcxXzQ0MSkiPgo8cGF0aCBkPSJNMTcwLjI4MyA0OEgxOTYuNUMyMDMuMTI3IDQ4IDIwOC41IDQyLjYyNzQgMjA4LjUgMzZWMTJDMjA4LjUgNS4zNzI1OCAyMDMuMTI3IDAgMTk2LjUgMEgxNzAuMjgzQzEyNi4xIDAgOTAuMjgzIDM1LjgxNzIgOTAuMjgzIDgwVjE3NkM5MC4yODMgMjA2LjkyOCA2NS4yMTA5IDIzMiAzNC4yODMgMjMySDIzQzE2LjM3MjYgMjMyIDExIDIzNy4zNzIgMTEgMjQ0VjI2OEMxMSAyNzQuNjI3IDE2LjM3MjQgMjgwIDIyLjk5OTYgMjgwTDM0LjI4MyAyODBDNjUuMjEwOSAyODAgOTAuMjgzIDMwNS4wNzIgOTAuMjgzIDMzNlY0NDBDOTAuMjgzIDQ3OS43NjQgMTIyLjUxOCA1MTIgMTYyLjI4MyA1MTJIMTk2LjVDMjAzLjEyNyA1MTIgMjA4LjUgNTA2LjYyNyAyMDguNSA1MDBWNDc2QzIwOC41IDQ2OS4zNzMgMjAzLjEyNyA0NjQgMTk2LjUgNDY0SDE2Mi4yODNDMTQ5LjAyOCA0NjQgMTM4LjI4MyA0NTMuMjU1IDEzOC4yODMgNDQwVjMzNkMxMzguMjgzIDMwOS4wMjIgMTI4LjAxMSAyODQuNDQzIDExMS4xNjQgMjY1Ljk2MUMxMDYuMTA5IDI2MC40MTYgMTA2LjEwOSAyNTEuNTg0IDExMS4xNjQgMjQ2LjAzOUMxMjguMDExIDIyNy41NTcgMTM4LjI4MyAyMDIuOTc4IDEzOC4yODMgMTc2VjgwQzEzOC4yODMgNjIuMzI2OSAxNTIuNjEgNDggMTcwLjI4MyA0OFoiIGZpbGw9IiNGRjk5MjIiLz4KPHBhdGggZD0iTTMwNSAzNkMzMDUgNDIuNjI3NCAzMTAuMzczIDQ4IDMxNyA0OEgzNDIuOTc5QzM2MC42NTIgNDggMzc0Ljk3OCA2Mi4zMjY5IDM3NC45NzggODBWMTc2QzM3NC45NzggMjAyLjk3OCAzODUuMjUxIDIyNy41NTcgNDAyLjA5OCAyNDYuMDM5QzQwNy4xNTMgMjUxLjU4NCA0MDcuMTUzIDI2MC40MTYgNDAyLjA5OCAyNjUuOTYxQzM4NS4yNTEgMjg0LjQ0MyAzNzQuOTc4IDMwOS4wMjIgMzc0Ljk3OCAzMzZWNDMyQzM3NC45NzggNDQ5LjY3MyAzNjAuNjUyIDQ2NCAzNDIuOTc5IDQ2NEgzMTdDMzEwLjM3MyA0NjQgMzA1IDQ2OS4zNzMgMzA1IDQ3NlY1MDBDMzA1IDUwNi42MjcgMzEwLjM3MyA1MTIgMzE3IDUxMkgzNDIuOTc5QzM4Ny4xNjEgNTEyIDQyMi45NzggNDc2LjE4MyA0MjIuOTc4IDQzMlYzMzZDNDIyLjk3OCAzMDUuMDcyIDQ0OC4wNTEgMjgwIDQ3OC45NzkgMjgwSDQ5MEM0OTYuNjI3IDI4MCA1MDIgMjc0LjYyOCA1MDIgMjY4VjI0NEM1MDIgMjM3LjM3MyA0OTYuNjI4IDIzMiA0OTAgMjMyTDQ3OC45NzkgMjMyQzQ0OC4wNTEgMjMyIDQyMi45NzggMjA2LjkyOCA0MjIuOTc4IDE3NlY4MEM0MjIuOTc4IDM1LjgxNzIgMzg3LjE2MSAwIDM0Mi45NzkgMEgzMTdDMzEwLjM3MyAwIDMwNSA1LjM3MjU4IDMwNSAxMlYzNloiIGZpbGw9IiNGRjk5MjIiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTcxXzQ0MSI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo="},"displayName":"Code","typeVersion":2,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":1119,"icon":"fa:robot","name":"@n8n/n8n-nodes-langchain.agent","codex":{"data":{"alias":["LangChain","Chat","Conversational","Plan and Execute","ReAct","Tools"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Agents","Root Nodes"]}}},"group":"[\"transform\"]","defaults":{"name":"AI Agent","color":"#404040"},"iconData":{"icon":"robot","type":"icon"},"displayName":"AI Agent","typeVersion":3,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1153,"icon":"file:openAiLight.svg","name":"@n8n/n8n-nodes-langchain.lmChatOpenAi","codex":{"data":{"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatopenai/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Language Models","Root Nodes"],"Language Models":["Chat Models (Recommended)"]}}},"group":"[\"transform\"]","defaults":{"name":"OpenAI Chat Model"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTM2Ljg2NzEgMTYuMzcxOEMzNy43NzQ2IDEzLjY0OCAzNy40NjIxIDEwLjY2NDIgMzYuMDEwOCA4LjE4NjYxQzMzLjgyODIgNC4zODY1MyAyOS40NDA3IDIuNDMxNDkgMjUuMTU1NiAzLjM1MTUxQzIzLjI0OTMgMS4yMDM5NiAyMC41MTA1IC0wLjAxNzMxNDggMTcuNjM5MiAwLjAwMDE4NTUzM0MxMy4yNTkxIC0wLjAwOTgxNDY4IDkuMzcyNzMgMi44MTAyNSA4LjAyNTIgNi45Nzc4M0M1LjIxMTM5IDcuNTU0MSAyLjc4MjU4IDkuMzE1MzggMS4zNjEzIDExLjgxMTdDLTAuODM3NDkzIDE1LjYwMTggLTAuMzM2MjMyIDIwLjM3OTQgMi42MDEzMyAyMy42Mjk0QzEuNjkzODEgMjYuMzUzMiAyLjAwNjMyIDI5LjMzNzEgMy40NTc2IDMxLjgxNDZDNS42NDAxNSAzNS42MTQ3IDEwLjAyNzcgMzcuNTY5NyAxNC4zMTI4IDM2LjY0OTdDMTYuMjE3OSAzOC43OTczIDE4Ljk1NzkgNDAuMDE4NSAyMS44MjkyIDM5Ljk5OThDMjYuMjExOCA0MC4wMTEgMzAuMDk5NCAzNy4xODg1IDMxLjQ0NjkgMzMuMDE3MUMzNC4yNjA4IDMyLjQ0MDkgMzYuNjg5NiAzMC42Nzk2IDM4LjExMDggMjguMTgzM0M0MC4zMDcxIDI0LjM5MzIgMzkuODA0NiAxOS42MTk0IDM2Ljg2ODMgMTYuMzY5M0wzNi44NjcxIDE2LjM3MThaTTIxLjgzMTcgMzcuMzg2QzIwLjA3OCAzNy4zODg1IDE4LjM3OTIgMzYuNzc0NyAxNy4wMzI5IDM1LjY1MDlDMTcuMDk0MSAzNS42MTg0IDE3LjIwMDQgMzUuNTU5NyAxNy4yNjkxIDM1LjUxNzJMMjUuMjM0MyAzMC45MTcxQzI1LjY0MTggMzAuNjg1OCAyNS44OTE4IDMwLjI1MjEgMjUuODg5MyAyOS43ODMzVjE4LjU1NDNMMjkuMjU1NyAyMC40OTgxQzI5LjI5MTkgMjAuNTE1NiAyOS4zMTU3IDIwLjU1MDYgMjkuMzIwNyAyMC41OTA2VjI5Ljg4OTZDMjkuMzE1NyAzNC4wMjQ3IDI1Ljk2NjggMzcuMzc3MiAyMS44MzE3IDM3LjM4NlpNNS43MjY0IDMwLjUwNzFDNC44NDc2MyAyOC45ODk2IDQuNTMxMzcgMjcuMjEwOCA0LjgzMjYzIDI1LjQ4NDVDNC44OTEzOCAyNS41MTk1IDQuOTk1MTMgMjUuNTgzMiA1LjA2ODg4IDI1LjYyNTdMMTMuMDM0MSAzMC4yMjU4QzEzLjQzNzggMzAuNDYyMSAxMy45Mzc4IDMwLjQ2MjEgMTQuMzQyOCAzMC4yMjU4TDI0LjA2NjggMjQuNjEwN1YyOC40OTgzQzI0LjA2OTMgMjguNTM4MyAyNC4wNTA1IDI4LjU3NyAyNC4wMTkzIDI4LjYwMkwxNS45Njc5IDMzLjI1MDlDMTIuMzgxNSAzNS4zMTU5IDcuODAxNDQgMzQuMDg4NCA1LjcyNzY1IDMwLjUwNzFINS43MjY0Wk0zLjYzMDEgMTMuMTIwNUM0LjUwNTEyIDExLjYwMDQgNS44ODY0IDEwLjQzNzkgNy41MzE0NCA5LjgzNDE1QzcuNTMxNDQgOS45MDI5IDcuNTI3NjkgMTAuMDI0MiA3LjUyNzY5IDEwLjEwOTJWMTkuMzEwNkM3LjUyNTE5IDE5Ljc3ODEgNy43NzUxOSAyMC4yMTE5IDguMTgxNDUgMjAuNDQzMUwxNy45MDU0IDI2LjA1N0wxNC41MzkxIDI4LjAwMDhDMTQuNTA1MyAyOC4wMjMzIDE0LjQ2MjggMjguMDI3IDE0LjQyNTMgMjguMDEwOEw2LjM3MjY2IDIzLjM1ODJDMi43OTM4MyAyMS4yODU2IDEuNTY2MzEgMTYuNzA2OCAzLjYyODg1IDEzLjEyMTdMMy42MzAxIDEzLjEyMDVaTTMxLjI4ODIgMTkuNTU2OUwyMS41NjQyIDEzLjk0MTdMMjQuOTMwNiAxMS45OTkyQzI0Ljk2NDMgMTEuOTc2NyAyNS4wMDY4IDExLjk3MjkgMjUuMDQ0MyAxMS45ODkyTDMzLjA5NyAxNi42MzhDMzYuNjgyMSAxOC43MDkzIDM3LjkxMDggMjMuMjk1NyAzNS44Mzk1IDI2Ljg4MDhDMzQuOTYzMyAyOC4zOTgzIDMzLjU4MzIgMjkuNTYwOCAzMS45Mzk1IDMwLjE2NThWMjAuNjg5NEMzMS45NDMyIDIwLjIyMTkgMzEuNjk0NSAxOS43ODk0IDMxLjI4OTQgMTkuNTU2OUgzMS4yODgyWk0zNC42MzgzIDE0LjUxNDJDMzQuNTc5NSAxNC40NzggMzQuNDc1OCAxNC40MTU1IDM0LjQwMiAxNC4zNzNMMjYuNDM2OCA5Ljc3Mjg5QzI2LjAzMzEgOS41MzY2NCAyNS41MzMxIDkuNTM2NjQgMjUuMTI4MSA5Ljc3Mjg5TDE1LjQwNDEgMTUuMzg4VjExLjUwMDRDMTUuNDAxNiAxMS40NjA0IDE1LjQyMDQgMTEuNDIxNyAxNS40NTE2IDExLjM5NjdMMjMuNTAzIDYuNzUxNThDMjcuMDg5NCA0LjY4Mjc5IDMxLjY3NDUgNS45MTQwNiAzMy43NDIgOS41MDE2NEMzNC42MTU4IDExLjAxNjcgMzQuOTMyIDEyLjc5MDUgMzQuNjM1OCAxNC41MTQySDM0LjYzODNaTTEzLjU3NDEgMjEuNDQzMUwxMC4yMDY1IDE5LjQ5OTRDMTAuMTcwMiAxOS40ODE5IDEwLjE0NjUgMTkuNDQ2OCAxMC4xNDE1IDE5LjQwNjhWMTAuMTA3OUMxMC4xNDQgNS45Njc4MSAxMy41MDI4IDIuNjEyNzQgMTcuNjQyOSAyLjYxNTI0QzE5LjM5NDIgMi42MTUyNCAyMS4wODkyIDMuMjMwMjUgMjIuNDM1NSA0LjM1MDI4QzIyLjM3NDMgNC4zODI3OCAyMi4yNjkzIDQuNDQxNTMgMjIuMTk5MiA0LjQ4NDAzTDE0LjIzNDEgOS4wODQxM0MxMy44MjY2IDkuMzE1MzggMTMuNTc2NiA5Ljc0Nzg5IDEzLjU3OTEgMTAuMjE2N0wxMy41NzQxIDIxLjQ0MDZWMjEuNDQzMVpNMTUuNDAyOSAxNy41MDA2TDE5LjczNDIgMTQuOTk5M0wyNC4wNjU1IDE3LjQ5OTNWMjIuNTAwN0wxOS43MzQyIDI1LjAwMDdMMTUuNDAyOSAyMi41MDA3VjE3LjUwMDZaIiBmaWxsPSIjN0Q3RDg3Ii8+Cjwvc3ZnPgo="},"displayName":"OpenAI Chat Model","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1179,"icon":"fa:code","name":"@n8n/n8n-nodes-langchain.outputParserStructured","codex":{"data":{"alias":["json","zod"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.outputparserstructured/"}]},"categories":["AI","Langchain"],"subcategories":{"AI":["Output Parsers"]}}},"group":"[\"transform\"]","defaults":{"name":"Structured Output Parser"},"iconData":{"icon":"code","type":"icon"},"displayName":"Structured Output Parser","typeVersion":1,"nodeCategories":[{"id":25,"name":"AI"},{"id":26,"name":"Langchain"}]},{"id":1235,"icon":"file:extractFromFile.svg","name":"n8n-nodes-base.extractFromFile","codex":{"data":{"alias":["CSV","Spreadsheet","Excel","xls","xlsx","ods","tabular","decode","decoding","Move Binary Data","Binary","File","PDF","JSON","HTML","ICS","iCal","txt","Text","RTF","XML","64","Base64","Convert"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.extractfromfile/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Files","Data Transformation"]}}},"group":"[\"input\"]","defaults":{"name":"Extract from File"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAuOTM3NSAyQzAuNDE5NzMzIDIgMCAyLjQxOTczIDAgMi45Mzc1VjM3LjMyMjFDMCAzNy44Mzk5IDAuNDE5NzMzIDM4LjI1OTYgMC45Mzc1IDM4LjI1OTZIMjYuMjE1NEMyNi43MzMyIDM4LjI1OTYgMjcuMTUyOSAzNy44Mzk5IDI3LjE1MjkgMzcuMzIyMUwyNy4xNTI5IDMwLjY3MTlMMTYuNzk2OSAzMC42NzE5QzE0Ljg5ODQgMzAuNjcxOSAxMy4zNTk0IDI5LjEzMjkgMTMuMzU5NCAyNy4yMzQ0VjI1LjM1OTRDMTMuMzU5NCAyMy40NjA5IDE0Ljg5ODQgMjEuOTIxOSAxNi43OTY5IDIxLjkyMTlIMjcuMTUyOUwyNy4xNTI5IDE1Ljc4MjFIMTQuMzA4M0MxMy43OTA2IDE1Ljc4MjEgMTMuMzcwOCAxNS4zNjI0IDEzLjM3MDggMTQuODQ0NlYySDAuOTM3NVoiIGZpbGw9IiMzNTNGNkUiLz4KPHBhdGggZD0iTTE2LjAyNzEgMkMxNS45NDA4IDIgMTUuODcwOCAyLjA2OTk2IDE1Ljg3MDggMi4xNTYyNVYxMi44MTM0QzE1Ljg3MDggMTMuMDcyMyAxNi4wODA3IDEzLjI4MjEgMTYuMzM5NiAxMy4yODIxSDI2Ljk5NjdDMjcuMDgzIDEzLjI4MjEgMjcuMTUyOSAxMy4yMTIyIDI3LjE1MjkgMTMuMTI1OUwyNy4xNTI5IDEyLjYxNzFDMjcuMTUyOSAxMi4zNjg4IDI3LjA1NDUgMTIuMTMwNyAyNi44NzkxIDExLjk1NUwxNy4yMjI1IDIuMjc1MzhDMTcuMDQ2NiAyLjA5OTA4IDE2LjgwNzkgMiAxNi41NTg4IDJIMTYuMDI3MVoiIGZpbGw9IiMzNTNGNkUiLz4KPHBhdGggZD0iTTI5Ljc2NDIgMzQuNjUwM0MyOS4wMzQgMzMuOTE2IDI5LjAzNzQgMzIuNzI4OCAyOS43NzE2IDMxLjk5ODZMMzMuNjE5NyAyOC4xNzE5TDE2Ljc5NjkgMjguMTcxOUMxNi4yNzkxIDI4LjE3MTkgMTUuODU5NCAyNy43NTIxIDE1Ljg1OTQgMjcuMjM0NFYyNS4zNTk0QzE1Ljg1OTQgMjQuODQxNiAxNi4yNzkxIDI0LjQyMTkgMTYuNzk2OSAyNC40MjE5TDMzLjU0MTIgMjQuNDIxOUwyOS43NzE2IDIwLjY3MzNDMjkuMDM3NCAxOS45NDMxIDI5LjAzNCAxOC43NTU5IDI5Ljc2NDIgMTguMDIxNkMzMC40OTQ0IDE3LjI4NzQgMzEuNjgxNiAxNy4yODQgMzIuNDE1OSAxOC4wMTQyTDM5LjQ0NzEgMjUuMDA2NEMzOS44MDEgMjUuMzU4MyA0MCAyNS44MzY4IDQwIDI2LjMzNTlDNDAgMjYuODM1IDM5LjgwMSAyNy4zMTM1IDM5LjQ0NzEgMjcuNjY1NUwzMi40MTU5IDM0LjY1NzZDMzEuNjgxNiAzNS4zODc4IDMwLjQ5NDQgMzUuMzg0NSAyOS43NjQyIDM0LjY1MDNaIiBmaWxsPSIjMzUzRjZFIi8+Cjwvc3ZnPgo="},"displayName":"Extract from File","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]}],"categories":[{"id":35,"name":"Document Extraction"},{"id":49,"name":"AI Summarization"}],"image":[]}}