{
  "name": "Natural Language to SQL Query",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "text-to-sql",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "d4e5f6a7-4444-4000-8000-000000000001",
      "name": "Webhook - Natural Language Query",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT table_name, column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position",
        "options": {}
      },
      "id": "d4e5f6a7-4444-4000-8000-000000000002",
      "name": "PostgreSQL - Get Schema",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        480,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const schemaRows = $input.all().map(item => item.json);\nconst tables = {};\nfor (const row of schemaRows) {\n  if (!tables[row.table_name]) tables[row.table_name] = [];\n  tables[row.table_name].push(`${row.column_name} (${row.data_type}${row.is_nullable === 'YES' ? ', nullable' : ''})`);\n}\nconst schemaText = Object.entries(tables).map(([table, cols]) => `Table: ${table}\\n  Columns: ${cols.join(', ')}`).join('\\n\\n');\nconst userQuery = $('Webhook - Natural Language Query').first().json.body.query;\nreturn [{ json: { schemaText, userQuery } }];"
      },
      "id": "d4e5f6a7-4444-4000-8000-000000000003",
      "name": "Code - Format Schema",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        300
      ]
    },
    {
      "parameters": {
        "resource": "chat",
        "model": "gpt-4o",
        "messages": {
          "values": [
            {
              "content": "=You are a PostgreSQL query expert. Convert the following natural language question into a safe, read-only SQL query.\n\nRules:\n1. ONLY generate SELECT statements. Never generate INSERT, UPDATE, DELETE, DROP, ALTER, or any DDL/DML.\n2. Use proper JOIN syntax where needed.\n3. Add appropriate WHERE clauses, GROUP BY, ORDER BY as needed.\n4. Limit results to 100 rows maximum.\n5. Use aliases for readability.\n6. Handle NULL values appropriately.\n\nDatabase Schema:\n{{ $json.schemaText }}\n\nUser Question: {{ $json.userQuery }}\n\nReturn ONLY the SQL query, no explanations or markdown formatting."
            }
          ]
        },
        "options": {
          "temperature": 0.1,
          "maxTokens": 1024
        }
      },
      "id": "d4e5f6a7-4444-4000-8000-000000000004",
      "name": "OpenAI - Generate SQL",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.4,
      "position": [
        960,
        300
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "let sql = $input.first().json.message.content.trim();\n// Remove markdown code blocks if present\nsql = sql.replace(/^```(sql)?\\n?/i, '').replace(/\\n?```$/i, '').trim();\n// Safety check: only allow SELECT statements\nconst upperSql = sql.toUpperCase().trim();\nif (!upperSql.startsWith('SELECT') && !upperSql.startsWith('WITH')) {\n  throw new Error('Generated query is not a SELECT statement. Aborting for safety.');\n}\nconst forbidden = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE', 'CREATE', 'GRANT', 'REVOKE'];\nfor (const word of forbidden) {\n  if (upperSql.includes(word + ' ')) {\n    throw new Error(`Forbidden keyword \"${word}\" found in query. Aborting.`);\n  }\n}\nreturn [{ json: { sql, originalQuery: $('Code - Format Schema').first().json.userQuery } }];"
      },
      "id": "d4e5f6a7-4444-4000-8000-000000000005",
      "name": "Code - Validate SQL Safety",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "={{ $json.sql }}",
        "options": {}
      },
      "id": "d4e5f6a7-4444-4000-8000-000000000006",
      "name": "PostgreSQL - Execute Query",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1440,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const results = $input.all().map(item => item.json);\nconst sql = $('Code - Validate SQL Safety').first().json.sql;\nconst originalQuery = $('Code - Validate SQL Safety').first().json.originalQuery;\nreturn [{ json: { success: true, originalQuery, generatedSql: sql, resultCount: results.length, results } }];"
      },
      "id": "d4e5f6a7-4444-4000-8000-000000000007",
      "name": "Code - Format Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1680,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}"
      },
      "id": "d4e5f6a7-4444-4000-8000-000000000008",
      "name": "Respond with Results",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1920,
        300
      ]
    }
  ],
  "connections": {
    "Webhook - Natural Language Query": {
      "main": [
        [
          {
            "node": "PostgreSQL - Get Schema",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PostgreSQL - Get Schema": {
      "main": [
        [
          {
            "node": "Code - Format Schema",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Format Schema": {
      "main": [
        [
          {
            "node": "OpenAI - Generate SQL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI - Generate SQL": {
      "main": [
        [
          {
            "node": "Code - Validate SQL Safety",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Validate SQL Safety": {
      "main": [
        [
          {
            "node": "PostgreSQL - Execute Query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PostgreSQL - Execute Query": {
      "main": [
        [
          {
            "node": "Code - Format Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Format Results": {
      "main": [
        [
          {
            "node": "Respond with Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [
    {
      "name": "ai-data"
    }
  ]
}