This workflow corresponds to n8n.io template #6629 — we link there as the canonical source.
This workflow follows the OpenAI → Postgres 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 →
{
"name": "Bank Statement Analyzer - Simplified",
"tags": [],
"nodes": [
{
"id": "webhook-001",
"name": "Upload Statement",
"type": "n8n-nodes-base.webhook",
"position": [
400,
300
],
"parameters": {
"path": "/upload-statement",
"options": {
"rawBody": true
},
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "file-handler-001",
"name": "File Handler",
"type": "n8n-nodes-base.code",
"position": [
600,
300
],
"parameters": {
"jsCode": "// Simple file processor\nconst inputData = $input.all()[0];\nconst files = [];\n\n// Handle file uploads\nif (inputData.binary) {\n Object.keys(inputData.binary).forEach(key => {\n const file = inputData.binary[key];\n files.push({\n filename: file.fileName,\n contentType: file.mimeType,\n uploadedAt: new Date().toISOString()\n });\n });\n}\n\nreturn files.map(file => ({\n json: {\n filename: file.filename,\n contentType: file.contentType,\n uploadedAt: file.uploadedAt,\n status: 'ready_for_processing'\n },\n binary: inputData.binary\n}));"
},
"typeVersion": 2
},
{
"id": "file-type-switch-001",
"name": "Check File Type",
"type": "n8n-nodes-base.if",
"position": [
800,
300
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "pdf-condition",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ $json.contentType }}",
"rightValue": "application/pdf"
}
]
}
},
"typeVersion": 2
},
{
"id": "pdf-extractor-001",
"name": "Extract PDF Text",
"type": "n8n-nodes-base.extractFromFile",
"position": [
1000,
200
],
"parameters": {
"operation": "extractText"
},
"typeVersion": 1
},
{
"id": "excel-parser-001",
"name": "Parse Excel/CSV",
"type": "n8n-nodes-base.spreadsheetFile",
"position": [
1000,
400
],
"parameters": {
"options": {
"headerRow": 0
},
"operation": "parseExcel"
},
"typeVersion": 2
},
{
"id": "ai-extractor-001",
"name": "AI Data Extractor",
"type": "n8n-nodes-base.openAi",
"position": [
1200,
300
],
"parameters": {
"model": "gpt-4o-mini",
"messages": {
"values": [
{
"role": "system",
"content": "Extract bank statement data and return clean JSON:\n\n{\n \"account_number\": \"****1234\",\n \"bank_name\": \"Bank Name\",\n \"statement_period\": \"2024-01-01 to 2024-01-31\",\n \"opening_balance\": 1500.00,\n \"closing_balance\": 1250.00,\n \"transactions\": [\n {\n \"date\": \"2024-01-15\",\n \"description\": \"GROCERY STORE\",\n \"amount\": -45.67,\n \"category\": \"groceries\"\n }\n ]\n}\n\nUse negative amounts for expenses, positive for income. Categorize transactions as: groceries, dining, gas, shopping, utilities, healthcare, entertainment, income, fees, or other."
},
{
"role": "user",
"content": "{{ $json.data || $json.extracted_text }}"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "data-processor-001",
"name": "Process & Summarize",
"type": "n8n-nodes-base.code",
"position": [
1400,
300
],
"parameters": {
"jsCode": "// Clean and validate extracted data\nconst inputData = $input.all()[0];\nlet extractedData = {};\n\n// Parse AI response\ntry {\n const content = inputData.json.message?.content || inputData.json;\n if (typeof content === 'string') {\n const jsonMatch = content.match(/{[\\s\\S]*}/);\n if (jsonMatch) {\n extractedData = JSON.parse(jsonMatch[0]);\n }\n } else {\n extractedData = content;\n }\n} catch (error) {\n console.log('Parse error:', error.message);\n extractedData = { transactions: [] };\n}\n\n// Clean transaction data\nconst cleanTransactions = (extractedData.transactions || []).map((tx, index) => ({\n id: `tx_${Date.now()}_${index}`,\n date: tx.date,\n description: (tx.description || '').trim().toUpperCase(),\n amount: parseFloat(tx.amount) || 0,\n category: tx.category || 'other',\n processed_at: new Date().toISOString()\n}));\n\n// Calculate summary\nconst totalExpenses = cleanTransactions\n .filter(tx => tx.amount < 0)\n .reduce((sum, tx) => sum + Math.abs(tx.amount), 0);\n\nconst totalIncome = cleanTransactions\n .filter(tx => tx.amount > 0)\n .reduce((sum, tx) => sum + tx.amount, 0);\n\nconst categoryTotals = {};\ncleanTransactions.forEach(tx => {\n if (tx.amount < 0) { // Only expenses\n categoryTotals[tx.category] = (categoryTotals[tx.category] || 0) + Math.abs(tx.amount);\n }\n});\n\nreturn [{\n json: {\n account_info: {\n account_number: extractedData.account_number || 'Unknown',\n bank_name: extractedData.bank_name || 'Unknown',\n statement_period: extractedData.statement_period || 'Unknown',\n opening_balance: parseFloat(extractedData.opening_balance) || 0,\n closing_balance: parseFloat(extractedData.closing_balance) || 0\n },\n transactions: cleanTransactions,\n summary: {\n total_transactions: cleanTransactions.length,\n total_expenses: totalExpenses,\n total_income: totalIncome,\n net_change: totalIncome - totalExpenses,\n category_breakdown: categoryTotals\n },\n processed_at: new Date().toISOString(),\n status: 'completed'\n }\n}]);"
},
"typeVersion": 2
},
{
"id": "save-to-db-001",
"name": "Save to Database",
"type": "n8n-nodes-base.postgres",
"position": [
1600,
200
],
"parameters": {
"table": "bank_statements",
"columns": {
"value": {
"raw_data": "={{ JSON.stringify($json) }}",
"bank_name": "={{ $json.account_info.bank_name }}",
"processed_at": "={{ $json.processed_at }}",
"total_income": "={{ $json.summary.total_income }}",
"account_number": "={{ $json.account_info.account_number }}",
"total_expenses": "={{ $json.summary.total_expenses }}",
"statement_period": "={{ $json.account_info.statement_period }}",
"total_transactions": "={{ $json.summary.total_transactions }}"
},
"mappingMode": "defineBelow"
},
"resource": "database",
"operation": "insert"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.4
},
{
"id": "response-001",
"name": "Send Response",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1600,
400
],
"parameters": {
"respondWith": "json",
"responseBody": "={\n \"success\": true,\n \"message\": \"Statement processed successfully\",\n \"data\": {\n \"account\": \"{{ $json.account_info.account_number }}\",\n \"transactions_processed\": {{ $json.summary.total_transactions }},\n \"total_expenses\": {{ $json.summary.total_expenses }},\n \"total_income\": {{ $json.summary.total_income }},\n \"categories\": {{ JSON.stringify($json.summary.category_breakdown) }}\n }\n}"
},
"typeVersion": 1
}
],
"notes": [
{
"id": "note-001",
"width": 160,
"height": 80,
"content": "\ud83d\udce5 **ENTRY POINT**\n\nUsers upload bank statements here via POST request",
"position": [
320,
180
]
},
{
"id": "note-002",
"width": 160,
"height": 100,
"content": "\ud83d\udd0d **FILE PREP**\n\nExtracts file info and prepares for processing. Handles multiple file formats.",
"position": [
520,
180
]
},
{
"id": "note-003",
"width": 160,
"height": 120,
"content": "\ud83d\udd00 **SMART ROUTING**\n\nPDFs go to text extraction\nExcel/CSV files go to spreadsheet parser\n\nAutomatic format detection",
"position": [
720,
120
]
},
{
"id": "note-004",
"width": 160,
"height": 90,
"content": "\ud83d\udcc4 **PDF HANDLER**\n\nExtracts text from PDF bank statements using OCR",
"position": [
920,
80
]
},
{
"id": "note-005",
"width": 160,
"height": 90,
"content": "\ud83d\udcca **SPREADSHEET HANDLER**\n\nParses Excel/CSV files and converts to structured data",
"position": [
920,
480
]
},
{
"id": "note-006",
"width": 160,
"height": 140,
"content": "\ud83e\udd16 **AI MAGIC**\n\nGPT-4 extracts:\n\u2022 Account details\n\u2022 All transactions \n\u2022 Auto-categorizes expenses\n\u2022 Calculates balances\n\nSmart & accurate!",
"position": [
1120,
140
]
},
{
"id": "note-007",
"width": 160,
"height": 120,
"content": "\ud83e\uddf9 **DATA CLEANUP**\n\nCleans & validates:\n\u2022 Transaction formatting\n\u2022 Amount calculations\n\u2022 Category summaries\n\u2022 Error handling",
"position": [
1320,
140
]
},
{
"id": "note-008",
"width": 160,
"height": 80,
"content": "\ud83d\udcbe **PERSISTENCE**\n\nSaves processed data to PostgreSQL database",
"position": [
1520,
80
]
},
{
"id": "note-009",
"width": 160,
"height": 100,
"content": "\u2705 **SUCCESS RESPONSE**\n\nReturns summary:\n\u2022 Transaction count\n\u2022 Expense totals\n\u2022 Category breakdown",
"position": [
1520,
480
]
},
{
"id": "note-010",
"width": 200,
"height": 120,
"content": "\ud83d\udca1 **WORKFLOW FEATURES**\n\n\u2713 Handles PDF & Excel files\n\u2713 AI-powered extraction \n\u2713 Auto-categorization\n\u2713 Database storage\n\u2713 Clean API responses",
"position": [
200,
450
]
},
{
"id": "note-011",
"width": 180,
"height": 140,
"content": "\ud83c\udfaf **TYPICAL OUTPUT**\n\n```json\n{\n \"success\": true,\n \"transactions_processed\": 45,\n \"total_expenses\": 2847.32,\n \"categories\": {\n \"groceries\": 450.23,\n \"dining\": 287.45\n }\n}\n```",
"position": [
1700,
240
]
}
],
"updatedAt": "2024-01-15T10:30:00.000Z",
"versionId": "1",
"staticData": null,
"connections": {
"File Handler": {
"main": [
[
{
"node": "Check File Type",
"type": "main",
"index": 0
}
]
]
},
"Check File Type": {
"main": [
[
{
"node": "Extract PDF Text",
"type": "main",
"index": 0
}
],
[
{
"node": "Parse Excel/CSV",
"type": "main",
"index": 0
}
]
]
},
"Parse Excel/CSV": {
"main": [
[
{
"node": "AI Data Extractor",
"type": "main",
"index": 0
}
]
]
},
"Extract PDF Text": {
"main": [
[
{
"node": "AI Data Extractor",
"type": "main",
"index": 0
}
]
]
},
"Upload Statement": {
"main": [
[
{
"node": "File Handler",
"type": "main",
"index": 0
}
]
]
},
"AI Data Extractor": {
"main": [
[
{
"node": "Process & Summarize",
"type": "main",
"index": 0
}
]
]
},
"Process & Summarize": {
"main": [
[
{
"node": "Save to Database",
"type": "main",
"index": 0
},
{
"node": "Send Response",
"type": "main",
"index": 0
}
]
]
}
},
"triggerCount": 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.
openAiApipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automatically processes bank statements from various formats and extracts structured transaction data with intelligent categorization using AI. File Upload - Accepts bank statements via webhook upload (PDF, Excel, CSV formats). Smart Format Detection -…
Source: https://n8n.io/workflows/6629/ — 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.
Postgres. Uses openAi, postgres, postgresTool, httpRequest. Webhook trigger; 19 nodes.
Crop Planning. Uses postgres, httpRequest, respondToWebhook, googleGemini. Webhook trigger; 11 nodes.
OTTO - Épico 1: Resolução Determinística de Identidade. Uses openAi, postgres, httpRequest. Webhook trigger; 11 nodes.
OTTO - Épico 2: Funil de Hipótese (IA Sugere, Sistema Julga). Uses openAi, postgres, httpRequest. Webhook trigger; 10 nodes.
Natural Language to SQL Query. Uses postgres, openAi. Webhook trigger; 8 nodes.