{
  "id": "uYYw1E8JAYRYC7sE",
  "name": "My workflow",
  "tags": [],
  "nodes": [
    {
      "id": "c46b2f23-abb5-4ae8-8f16-0ecfd39533d7",
      "name": "When chat message received",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        1344,
        464
      ],
      "parameters": {
        "public": true,
        "options": {}
      },
      "typeVersion": 1.1
    },
    {
      "id": "22f89ea8-71d0-40bf-bad1-24a803ddbdcb",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        352
      ],
      "parameters": {
        "color": 5,
        "width": 1232,
        "height": 624,
        "content": "## Database Query Agent\n"
      },
      "typeVersion": 1
    },
    {
      "id": "ec22405a-53d0-47ee-b667-2132ecce2a1a",
      "name": "Set Table Name",
      "type": "n8n-nodes-base.set",
      "position": [
        1600,
        464
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "25d58d22-7bf2-45eb-900b-5e35ced2d6d3",
              "name": "table_name",
              "type": "string",
              "value": "product_inventory"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "addd05ca-9db5-436c-93e8-9c08ce40bc14",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1616,
        768
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "ed57c796-1d8c-4b68-aa95-ee5a65f27b38",
      "name": "Simple Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        1808,
        768
      ],
      "parameters": {
        "sessionKey": "={{ $('When chat message received').item.json.sessionId }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    },
    {
      "id": "8ac733ea-97de-4bd3-b669-6235c4ec55c6",
      "name": "Execute SQL Query",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2000,
        768
      ],
      "parameters": {
        "query": "{{ $fromAI(\"sql_query\", \"SQL Query\") }}",
        "options": {},
        "operation": "executeQuery",
        "descriptionType": "manual",
        "toolDescription": "Fetch all the data from Postgres, ensuring that table names are always written with their appropriate schema prefix. Each table in the database belongs to a specific schema, so make sure to reference every table using its full schema-qualified name."
      },
      "typeVersion": 2.5
    },
    {
      "id": "e9e87bc9-048b-47fa-b52c-937563c74c1c",
      "name": "Get Table Definition",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2224,
        768
      ],
      "parameters": {
        "query": "select\n  c.column_name,\n  c.data_type,\n  c.is_nullable,\n  c.column_default,\n  tc.constraint_type,\n  ccu.table_name AS referenced_table,\n  ccu.column_name AS referenced_column\nfrom\n  information_schema.columns c\nLEFT join\n  information_schema.key_column_usage kcu\n  ON c.table_name = kcu.table_name\n  AND c.column_name = kcu.column_name\nLEFT join\n  information_schema.table_constraints tc\n  ON kcu.constraint_name = tc.constraint_name\n  AND tc.constraint_type = 'FOREIGN KEY'\nLEFT join\n  information_schema.constraint_column_usage ccu\n  ON tc.constraint_name = ccu.constraint_name\nwhere\n  c.table_name = '{{ $json.table_name }}'\n  AND c.table_schema = 'public'\norder by\n  c.ordinal_position",
        "options": {},
        "operation": "executeQuery",
        "descriptionType": "manual",
        "toolDescription": "Get table schema definition to find all columns and its types"
      },
      "executeOnce": true,
      "typeVersion": 2.5
    },
    {
      "id": "1246e3ac-9a70-4bd7-a736-bb8e0d82d757",
      "name": "Database Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1856,
        464
      ],
      "parameters": {
        "text": "={{ $('When chat message received').item.json.chatInput }}",
        "options": {
          "systemMessage": "=# AI Agent Database Query System - Dynamic Column Mapping\n\n# Role\nYou are an expert SQL assistant that can answer natural-language questions by querying a **single table**.  \nThe active table schema will always be provided via the **Get Table Definition** tool.  \nYour job is to understand the user\u2019s request, map it to the actual columns/values in that table, generate a safe SQL query, run it with **Execute SQL Query**, and return only the matching rows.  \nThis must work for any type of table (education, product inventory, HR, finance, etc.).\n\n# Workflow (internal \u2013 do not show to the user)\n1. Call **Get Table Definition** first to get the schema (`table` name and `columns`).  \n2. Identify which user terms map to which columns by inspecting column names + data types.  \n   - Examples:  \n     - \u201cbachelor/master/PhD\u201d \u2192 `course_level` / `level`  \n     - \u201cprice under 500\u201d \u2192 numeric/price/cost/fee column  \n     - \u201cin stock/available\u201d \u2192 quantity/stock/active/available columns  \n     - \u201cSydney/Melbourne\u201d \u2192 location/campus/city/state columns  \n     - Free-text terms \u2192 name/title/description columns  \n3. For each relevant column, run a **discovery query** to fetch distinct values (e.g. `SELECT DISTINCT col FROM {{ $json.table_name }} LIMIT 200`) and use fuzzy/ILIKE matching to align user terms with actual values.  \n4. Build a safe SQL query:  \n   - **FROM**: always the `{{ $json.table_name }}` from schema.  \n   - **SELECT**: `*` unless the user asks for specific columns (then only select existing ones).  \n   - **WHERE**:  \n     - String: `col ILIKE '%term%'`  \n     - Exact match if distinct value matches exactly  \n     - For multi-value/CSV/JSON text fields: `col::text ILIKE '%term%'`  \n     - Numeric filters: parse numbers from text; use `<`, `>`, `BETWEEN` with safe casting  \n     - Date filters: cast text to date if needed; support \u201cbefore/after/between/latest\u201d  \n     - Boolean filters: map \u201cyes/true/available/on\u201d \u2192 `true`; \u201cno/false/off\u201d \u2192 `false`  \n   - **ORDER BY**:  \n     - Prefer `updated_at` or `last_updated` desc if present and user asks for \u201clatest/recent\u201d  \n     - Or order by `created_at` if that exists  \n   - **LIMIT**: 50 by default; adjust only if user explicitly requests a number.  \n5. Run the query with **Execute SQL Query**.  \n6. Return only the resulting rows to the user.  \n7. If no results, reply: *\u201cNo records found matching your criteria.\u201d*\n\n# Output Rules\n- Do **not** show SQL, tool calls, or internal reasoning.  \n- Always return just the clean data results (table or JSON) to the user.\n\n# Example usage\n- User: \u201cShow bachelor IT courses in Sydney under 35,000\u201d  \n  \u2192 Return rows where course_level matches \u201cUndergraduate\u201d, course_name contains \u201cIT\u201d/\u201cInformation Technology\u201d, campus_location contains \u201cSydney\u201d, fee < 35000.\n\n- User: \u201cList in-stock laptops under $800 in Melbourne\u201d  \n  \u2192 Return rows where category/name contains \u201claptop\u201d, stock > 0, price < 800, location contains \u201cMelbourne\u201d.\n"
        },
        "promptType": "define"
      },
      "typeVersion": 2
    },
    {
      "id": "f13f2521-0c8a-41da-aaad-7c3e82fff652",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2496,
        352
      ],
      "parameters": {
        "color": 4,
        "width": 320,
        "height": 624,
        "content": "\n### Step 1: Prerequisites\n- n8n instance (cloud/self-hosted)\n- PostgreSQL database with read access\n- OpenAI API key/You can use other LLM as well\n\n### Step 2: Import & Configure\n1. Import this workflow template into n8n\n2. **Add Credentials**:\n   - OpenAI API: Add your API key\n   - PostgreSQL: Configure database connection\n3. **Set Table Name**: Edit \"Set Table Name\" node \u2192 Replace `\"table_name\"` with your actual table\n4. **Test Connection**: Ensure your database user has SELECT permissions\n\n### Step 3: Deploy & Use\n1. Start the workflow\n2. Open the chat interface\n3. Ask questions like:\n   - *\"Show all active users\"*\n   - *\"Find orders from last month over $100\"*\n   - *\"List products with low inventory\"*"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "4043d2be-67cd-491d-854a-cec912f59ec0",
  "connections": {
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "Database Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Set Table Name": {
      "main": [
        [
          {
            "node": "Database Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute SQL Query": {
      "ai_tool": [
        [
          {
            "node": "Database Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Database Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Get Table Definition": {
      "ai_tool": [
        [
          {
            "node": "Database Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Set Table Name",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}