This workflow corresponds to n8n.io template #14057 — we link there as the canonical source.
This workflow follows the Agent → Chat Trigger recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "A3a9KLboMNFMHd5h",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "AI SQL Analytics Agent: Chat With Any PostgreSQL Database Using Natural Language",
"tags": [],
"nodes": [
{
"id": "d8e5561d-5ac8-4cc0-970e-a1bb9238b9bd",
"name": "Chat Trigger",
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"position": [
-768,
176
],
"parameters": {
"public": true,
"options": {}
},
"typeVersion": 1.4
},
{
"id": "ab7f525e-93ba-441d-8b94-d5a4260e81d3",
"name": "Manage Table Name",
"type": "n8n-nodes-base.code",
"position": [
-544,
176
],
"parameters": {
"jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// TABLE NAME MANAGER v4.0\n//\n// Changes vs v3.2:\n// - Static data keyed by sessionId \u2192 no cross-user pollution\n// - Schema-prefix stripping (public.orders \u2192 orders)\n// - Common-word blocklist prevents \"hello\" being saved as table\n// - Emits same fields as before (backward-compat)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst staticData = $getWorkflowStaticData('global');\nconst sessionId = ($input.first().json.sessionId ?? 'default').replace(/[^a-zA-Z0-9_-]/g, '_');\nconst userMessage = ($input.first().json.chatInput ?? '').trim();\n\n// Per-session storage\nif (!staticData.sessions) staticData.sessions = {};\nif (!staticData.sessions[sessionId]) staticData.sessions[sessionId] = {};\nconst sess = staticData.sessions[sessionId];\n\n// \u2500\u2500 RESET \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (/^(reset|change|switch)\\s+table$/i.test(userMessage)) {\n delete sess.tableName;\n return [{\n json: { chatInput: userMessage, tableName: null,\n justSetTable: false, isReset: true, sessionId }\n }];\n}\n\n// \u2500\u2500 CAPTURE TABLE NAME \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Strip optional schema prefix (public.my_table \u2192 my_table)\nlet candidate = userMessage;\nconst dotIdx = candidate.indexOf('.');\nif (dotIdx !== -1) candidate = candidate.slice(dotIdx + 1);\n\nconst looksLikeIdentifier = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(candidate);\n\n// Blocklist \u2014 common greetings that match the identifier pattern\nconst RESERVED = new Set([\n 'hello','hi','hey','help','yes','no','ok','okay','thanks',\n 'sure','start','begin','go','done','exit','quit','stop',\n 'what','show','list','get','find','give','tell','run'\n]);\n\nlet justSetTable = false;\nif (!sess.tableName && looksLikeIdentifier && !RESERVED.has(candidate.toLowerCase())) {\n sess.tableName = candidate; // store the clean (no-schema) name\n justSetTable = true;\n}\n\nreturn [{\n json: {\n chatInput: userMessage,\n tableName: sess.tableName ?? null,\n justSetTable,\n isReset: false,\n sessionId\n }\n}];"
},
"typeVersion": 2
},
{
"id": "03d7146e-1009-4ccf-90e0-6a241d04c556",
"name": "Need Schema?",
"type": "n8n-nodes-base.if",
"position": [
-320,
176
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-has-table",
"operator": {
"type": "string",
"operation": "notEmpty"
},
"leftValue": "={{ $json.tableName }}",
"rightValue": ""
},
{
"id": "cond-not-justset",
"operator": {
"type": "boolean",
"operation": "notEquals"
},
"leftValue": "={{ $json.justSetTable }}",
"rightValue": true
},
{
"id": "cond-not-reset",
"operator": {
"type": "boolean",
"operation": "notEquals"
},
"leftValue": "={{ $json.isReset }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "ee74c609-c31a-49bf-928a-c3da7bd7a8ef",
"name": "Fetch Schema",
"type": "n8n-nodes-base.postgres",
"position": [
-96,
80
],
"parameters": {
"query": "SELECT\n column_name,\n data_type,\n is_nullable,\n column_default\nFROM information_schema.columns\nWHERE\n table_schema = 'public'\n AND table_name = '{{ $('Manage Table Name').item.json.tableName }}'\nORDER BY ordinal_position\nLIMIT 120",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "a245eb4f-de7e-4e2c-9e2b-34b947b51d28",
"name": "Format Schema",
"type": "n8n-nodes-base.code",
"position": [
128,
80
],
"parameters": {
"jsCode": "\nconst tableCtx = $('Manage Table Name').first().json;\nconst schemaRows = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\nlet columnList = '';\nlet validColumnNames = [];\n\nif (schemaRows.length === 0 || !schemaRows[0].json.column_name) {\n columnList = `\u26a0\ufe0f Table \"${tableCtx.tableName}\" was not found in schema public.\\nPlease check the table name and try again, or type \\`reset table\\` to start over.`;\n staticData.validColumns = [];\n staticData.validTableName = null;\n} else {\n const columns = schemaRows.map(row => ({\n name: row.json.column_name,\n dtype: row.json.data_type,\n nullable: row.json.is_nullable === 'YES' ? 'nullable' : 'NOT NULL',\n defVal: row.json.column_default ? ` | default: ${row.json.column_default}` : '',\n isMoney: row.json.data_type === 'money'\n }));\n\n validColumnNames = columns.map(c => c.name);\n staticData.validColumns = validColumnNames;\n staticData.validTableName = tableCtx.tableName;\n\n const truncated = schemaRows.length >= 120;\n columnList = columns\n .map(c => {\n const castHint = c.isMoney ? ' \u26a0\ufe0f money\u2192cast: col::numeric' : '';\n return ` - ${c.name.padEnd(28)} (${c.dtype}, ${c.nullable}${c.defVal}${castHint})`;\n })\n .join('\\n');\n if (truncated) columnList += '\\n \u2026 (first 120 columns shown)';\n}\n\n// Build shorthand \u2192 exact column map\nconst suffixes = ['_eur','_usd','_gbp','_share','_growth','_index',\n '_price','_count','_total','_rate','_pct','_percent',\n '_avg','_sum','_min','_max','_id','_code','_name'];\nconst mappings = [];\nfor (const name of validColumnNames) {\n const n = name.toLowerCase();\n for (const suffix of suffixes) {\n if (n.endsWith(suffix)) {\n const shorthand = n.slice(0, n.length - suffix.length);\n if (shorthand.length > 1) {\n mappings.push(` If user says \"${shorthand}\" \u2192 exact column: ${name}`);\n }\n break;\n }\n }\n}\nconst conceptMap = mappings.length > 0\n ? mappings.join('\\n')\n : ' (all column names are self-explanatory)';\n\nreturn [{\n json: {\n chatInput: tableCtx.chatInput,\n tableName: tableCtx.tableName,\n justSetTable: tableCtx.justSetTable,\n isReset: tableCtx.isReset,\n columnList,\n conceptMap\n }\n}];"
},
"typeVersion": 2
},
{
"id": "b1385865-6234-4727-a5fa-0a010ef53b8e",
"name": "Empty Schema",
"type": "n8n-nodes-base.set",
"position": [
128,
272
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "empty-col-list",
"name": "columnList",
"type": "string",
"value": "(schema not loaded \u2014 table not yet configured)"
},
{
"id": "empty-concept-map",
"name": "conceptMap",
"type": "string",
"value": "(schema not loaded \u2014 table not yet configured)"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "448e26ba-7e19-4b7a-8f3f-31f557743264",
"name": "Unified Analytics Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
472,
176
],
"parameters": {
"options": {
"systemMessage": "You are an AI data analytics assistant for PostgreSQL data.\n\n## STATE\nTable: {{ $json.tableName ?? 'NOT SET' }} | justSet: {{ $json.justSetTable }} | reset: {{ $json.isReset ?? false }}\nUser: {{ $json.chatInput }}\n\n## COLUMNS \u2014 EXACT NAMES ONLY (copy character-for-character into SQL)\n{{ $json.columnList }}\n\n## SHORTHAND TRANSLATIONS (user says \u2192 SQL must use)\n{{ $json.conceptMap ?? '(schema not loaded)' }}\n\n\u26d4 NEVER use a column not listed above. \"revenue\" \u2260 \"revenue_eur\". Use EXACT names only.\n\u26d4 If columnList contains \"\u26a0\ufe0f Table ... was not found\", do NOT call any tools. Tell the user the table was not found and suggest they type `reset table` to try again.\n\n---\n## STATES\n\n### \u2460 NO TABLE (tableName = NOT SET)\nNo tools. Reply: \"\ud83d\udc4b Type your PostgreSQL table name to begin. Type `reset table` anytime to switch.\"\n\n### \u2461 JUST SET (justSetTable = true)\nNo tools. Reply: \"\u2705 Table `{{ $json.tableName }}` connected!\nColumns:\n{{ $json.columnList }}\nAsk me anything!\"\n\n### \u2462 RESET (isReset = true)\nNo tools. Reply: \"\ud83d\udd04 Cleared! Type your new table name.\"\n\n### \u2463 TABLE NOT FOUND (columnList starts with \u26a0\ufe0f)\nNo tools. Reply: \"\u274c Table `{{ $json.tableName }}` was not found. Please check the name and type `reset table` to try a different one.\"\n\n### \u2464 ANALYTICS (tableName set, justSetTable=false, isReset=false, no \u26a0\ufe0f)\n\n**Call `Execute SQL Query` \u2014 self-correcting loop (max 2 calls total):**\n- Write SQL using ONLY the exact column names from the COLUMNS list above\n- If the tool returns `{ \"error\": \"...\" }`, diagnose and fix:\n \u2022 \"column ... does not exist\" \u2192 find the correct name in COLUMNS list above\n \u2022 \"function avg(money) does not exist\" \u2192 cast the column: col::numeric\n \u2022 \"function round ... does not exist\" \u2192 cast before ROUND: ROUND(val::numeric, 2)\n \u2022 \"operator does not exist: money ...\" \u2192 cast money column: col::numeric\n \u2022 \"syntax error\" \u2192 review and fix the SQL structure\n \u2022 Any other error \u2192 explain to user in plain English, do not retry\n- After fixing, call the tool exactly one more time\n- Never call the tool more than twice total\n- Never show the raw error or SQL to the user\n\n**SQL rules:**\n- No backticks, no semicolons, no markdown\n- ROUND needs cast: ROUND(AVG(col)::numeric, 2) \u2705 | ROUND(AVG(col), 2) \u274c\n- **money columns (marked \u26a0\ufe0f money\u2192cast in the COLUMNS list) MUST be cast before ANY aggregate:**\n AVG(col::numeric) \u2705 | SUM(col::numeric) \u2705 | ROUND(AVG(col::numeric)::numeric,2) \u2705\n AVG(col) \u274c \u2014 \"function avg(money) does not exist\"\n- SUM text columns: SUM(col::numeric)\n- Aliases: SUM(units_sold) AS total_units_sold\n- ORDER BY on ranked/time queries | LIMIT 20 for lists\n- CTEs for year-over-year\n\n**Output rules:**\n- Numbers: 1,234,567 | EUR: \u20ac1,234 | Ratios: 12.5%\n- Tables or bullets for multi-row\n- Never mention SQL, queries, or database internals\n- use Quick chart tool to create chat and always reply with chart also when giving the data"
}
},
"typeVersion": 3.1
},
{
"id": "22f7dbf6-d64b-4951-ac16-2f066f3e2d6d",
"name": "OpenAI GPT-4o-mini",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
352,
400
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini"
},
"options": {
"temperature": 0
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "a8747d73-262a-4a18-a262-d01490f64b94",
"name": "Chat Memory",
"type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
"position": [
480,
400
],
"parameters": {
"sessionKey": "={{ $('Chat Trigger').item.json.sessionId }}",
"contextWindowLength": 6
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "cee02d80-045a-4cc9-abe6-44cb364130f7",
"name": "Execute SQL Query",
"type": "n8n-nodes-base.postgresTool",
"position": [
608,
400
],
"parameters": {
"query": "{{ $fromAI('sql_query', 'A complete valid PostgreSQL SELECT or WITH (CTE) query. No backticks, no semicolons.') }}",
"options": {},
"operation": "executeQuery",
"descriptionType": "manual",
"toolDescription": "Executes a PostgreSQL SELECT or WITH (CTE) query. Returns rows as JSON on success. On failure returns { \"error\": \"<postgres error message>\" } \u2014 read the error, fix the SQL (wrong column name, bad cast, syntax, etc.) and call this tool ONE more time with the corrected query. Never call it more than twice total. Provide a plain query \u2014 no backticks, no semicolons, no markdown."
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.5,
"continueOnFail": true
},
{
"id": "8ab38bf8-6dc2-4766-9ccf-16233bb46dd9",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-640,
688
],
"parameters": {},
"typeVersion": 1
},
{
"id": "627af582-602e-4043-894e-a58db835c379",
"name": "\u2699\ufe0f Config (Sync)",
"type": "n8n-nodes-base.set",
"position": [
-416,
688
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "sync-cfg-tablename",
"name": "table_name",
"type": "string",
"value": "bmw_global_sales"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "54eaaa6e-55a6-478f-b769-621ab8ab5260",
"name": "Fetch Google Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
32,
688
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BSx2AmyI7DAqxOwFcYT0kG70rueSY_QVm56LeCasRpU/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1BSx2AmyI7DAqxOwFcYT0kG70rueSY_QVm56LeCasRpU",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BSx2AmyI7DAqxOwFcYT0kG70rueSY_QVm56LeCasRpU/edit?usp=drivesdk",
"cachedResultName": "bmw_global_sales_2018_2025"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "6e2faa14-2ede-45d1-aad6-56064f6ae6f8",
"name": "Insert Rows to Postgres",
"type": "n8n-nodes-base.postgres",
"position": [
256,
688
],
"parameters": {
"table": {
"__rl": true,
"mode": "list",
"value": "bmw_global_sales",
"cachedResultName": "bmw_global_sales"
},
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"columns": {
"value": {
"year": "={{ $json.Year }}",
"model": "={{ $json.Model }}",
"month": "={{ $json.Month }}",
"region": "={{ $json.Region }}",
"bev_share": "={{ $json.BEV_Share }}",
"gdp_growth": "={{ $json.GDP_Growth }}",
"units_sold": "={{ $json.Units_Sold }}",
"revenue_eur": "={{ $json.Revenue_EUR }}",
"avg_price_eur": "={{ $json.Avg_Price_EUR }}",
"premium_share": "={{ $json.Premium_Share }}",
"fuel_price_index": "={{ $json.Fuel_Price_Index }}"
},
"schema": [
{
"id": "year",
"type": "number",
"display": true,
"required": true,
"displayName": "year",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "month",
"type": "number",
"display": true,
"required": true,
"displayName": "month",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "region",
"type": "string",
"display": true,
"required": false,
"displayName": "region",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "model",
"type": "string",
"display": true,
"required": false,
"displayName": "model",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "units_sold",
"type": "number",
"display": true,
"required": false,
"displayName": "units_sold",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "avg_price_eur",
"type": "string",
"display": true,
"required": false,
"displayName": "avg_price_eur",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "revenue_eur",
"type": "string",
"display": true,
"required": false,
"displayName": "revenue_eur",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "bev_share",
"type": "number",
"display": true,
"required": false,
"displayName": "bev_share",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "premium_share",
"type": "number",
"display": true,
"required": false,
"displayName": "premium_share",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "gdp_growth",
"type": "number",
"display": true,
"required": false,
"displayName": "gdp_growth",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "fuel_price_index",
"type": "number",
"display": true,
"required": false,
"displayName": "fuel_price_index",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "7c0df0b0-8986-4537-a9a0-cdb2c4907c2b",
"name": "Truncate Table",
"type": "n8n-nodes-base.postgres",
"position": [
-192,
688
],
"parameters": {
"query": "TRUNCATE TABLE {{ $json.table_name }}",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "3efea6d1-50fb-47a4-8bc1-ca2d58184a20",
"name": "QuickChart",
"type": "n8n-nodes-base.quickChartTool",
"position": [
736,
400
],
"parameters": {
"data": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Data', ``, 'json') }}",
"chartOptions": {},
"datasetOptions": {}
},
"typeVersion": 1
},
{
"id": "2199f7a4-bf7f-4459-82df-0bc099f54453",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1248,
-48
],
"parameters": {
"width": 416,
"height": 784,
"content": "This workflow lets you query your PostgreSQL database using natural language.\n\nInstead of writing SQL manually or exporting data, users can simply type questions and receive structured answers with charts. The workflow dynamically reads the table schema, ensures only valid columns are used, and generates safe SQL queries.\n\nUsers can connect to any table directly from chat by typing the table name. The workflow remembers the table for the session, so it does not need to be repeated. To switch tables, users can type `reset table` at any time.\n\n### How it works\n- User sends a message via chat\n- Table name is captured and stored per session\n- Schema is fetched once and reused for efficiency\n- AI generates SQL using only valid columns\n- Query is executed and results are returned\n- Charts are generated for visualization\n\n### Setup\n1. Connect your PostgreSQL credentials\n2. Ensure your table exists in the `public` schema\n3. (Optional) Configure Google Sheets sync for data ingestion\n4. Replace the default table name if using sync workflow\n5. Start by typing a table name, then ask questions\n\n### Customization\n- Modify system prompt for stricter SQL rules\n- Extend to support multiple schemas\n- Replace the LLM with any supported model"
},
"typeVersion": 1
},
{
"id": "a884bdb7-b101-46d9-a0b5-2c57ddbe640c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-792,
48
],
"parameters": {
"color": "#FFFFFF",
"width": 368,
"height": 288,
"content": "Handles user input and session tracking \nStores table name to avoid repeated input"
},
"typeVersion": 1
},
{
"id": "7a5c3be7-d047-4414-9e23-07e641d23184",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-360,
-16
],
"parameters": {
"color": "#FFFFFF",
"width": 624,
"height": 448,
"content": "Handles user input and session tracking \nStores table name to avoid repeated input"
},
"typeVersion": 1
},
{
"id": "f94c9c4d-dfca-4058-9bd9-f91368d28040",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
272,
16
],
"parameters": {
"color": "#FFFFFF",
"width": 624,
"height": 528,
"content": "Generates SQL from natural language \nExecutes queries and formats results\nCreates charts from query results \nHelps interpret data visually"
},
"typeVersion": 1
},
{
"id": "a82e2581-bdce-43ed-89c5-721394753221",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-704,
592
],
"parameters": {
"color": "#FFFFFF",
"width": 1184,
"height": 288,
"content": "Loads data from Google Sheets into PostgreSQL \nUseful for scheduled or batch updates"
},
"typeVersion": 1
},
{
"id": "12a03ce6-1ce8-4a0a-a2a6-c30f9bcea08e",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-272,
608
],
"parameters": {
"color": "#BE2727",
"height": 240,
"content": "\u26a0\ufe0f Truncate Table will delete existing data \nDisable or remove if not required"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "9c944b49-4572-4fe7-943e-d62572be8827",
"connections": {
"QuickChart": {
"ai_tool": [
[
{
"node": "Unified Analytics Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Chat Memory": {
"ai_memory": [
[
{
"node": "Unified Analytics Agent",
"type": "ai_memory",
"index": 0
}
]
]
},
"Chat Trigger": {
"main": [
[
{
"node": "Manage Table Name",
"type": "main",
"index": 0
}
]
]
},
"Empty Schema": {
"main": [
[
{
"node": "Unified Analytics Agent",
"type": "main",
"index": 0
}
]
]
},
"Fetch Schema": {
"main": [
[
{
"node": "Format Schema",
"type": "main",
"index": 0
}
]
]
},
"Need Schema?": {
"main": [
[
{
"node": "Fetch Schema",
"type": "main",
"index": 0
}
],
[
{
"node": "Empty Schema",
"type": "main",
"index": 0
}
]
]
},
"Format Schema": {
"main": [
[
{
"node": "Unified Analytics Agent",
"type": "main",
"index": 0
}
]
]
},
"Manual Trigger": {
"main": [
[
{
"node": "\u2699\ufe0f Config (Sync)",
"type": "main",
"index": 0
}
]
]
},
"Truncate Table": {
"main": [
[
{
"node": "Fetch Google Sheet",
"type": "main",
"index": 0
}
]
]
},
"Execute SQL Query": {
"ai_tool": [
[
{
"node": "Unified Analytics Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Manage Table Name": {
"main": [
[
{
"node": "Need Schema?",
"type": "main",
"index": 0
}
]
]
},
"Fetch Google Sheet": {
"main": [
[
{
"node": "Insert Rows to Postgres",
"type": "main",
"index": 0
}
]
]
},
"OpenAI GPT-4o-mini": {
"ai_languageModel": [
[
{
"node": "Unified Analytics Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"\u2699\ufe0f Config (Sync)": {
"main": [
[
{
"node": "Truncate Table",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleSheetsOAuth2ApiopenAiApipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This template turns your PostgreSQL database into an AI-powered analytics assistant.
Source: https://n8n.io/workflows/14057/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
✨📊Multi-AI Agent Chatbot for Postgres/Supabase DB and QuickCharts + Tool Router. Uses chatTrigger, postgresTool, executeWorkflowTrigger, toolWorkflow. Chat trigger; 40 nodes.
This workflow is ideal for data analysts, developers, and business intelligence teams who need an AI-powered chatbot to query Postgres/Supabase databases and generate dynamic charts for data visualiza
https://www.youtube.com/watch?v=OwIFK-r-NtQ
HDW Lead Geländewagen. Uses chatTrigger, lmChatOpenAi, memoryBufferWindow, outputParserStructured. Chat trigger; 92 nodes.
Who’s it for Creators who want to create faceless videos automatically, while keeping human oversight and quality control.