Classification automatique des dépenses Revolut
Ceci est unContent Creation, Multimodal AIworkflow d'automatisation du domainecontenant 19 nœuds.Utilise principalement des nœuds comme Set, Code, Merge, Crypto, Supabase. Classer automatiquement les transactions Revolut avec l'IA et Airtable
- •URL et Clé API Supabase
- •Informations d'identification Google Drive API
- •Clé API OpenAI
Nœuds utilisés (19)
Catégorie
{
"id": "qfdeygimBULYjsqJ",
"meta": {
"instanceId": "c9b372d6af59e3f6d8835345664babb3bd2c029f9d3764b59df1829e2ae18ec7",
"templateCredsSetupCompleted": true
},
"name": "Revolut Expenses Manual",
"tags": [
{
"id": "ea4Bb1csGvuESWWb",
"name": "Revolut",
"createdAt": "2025-08-11T13:32:47.326Z",
"updatedAt": "2025-08-11T13:32:47.326Z"
}
],
"nodes": [
{
"id": "44cf1317-7bac-45f6-a139-6979e1923496",
"name": "When clicking ‘Execute workflow’",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-2352,
16
],
"parameters": {},
"typeVersion": 1
},
{
"id": "2ee0180a-e65f-4cf9-9d94-045844873105",
"name": "Note adhésive",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2960,
-352
],
"parameters": {
"width": 496,
"height": 800,
"content": "## Revolut Extracts Analyzer\n\n### This n8n template processes Revolut statements, normalizes transactions, and uses AI to categorize expenses automatically.\n### Use cases include detecting subscriptions, separating internal transfers, and building dashboards to track spending.\n---\n\n## How it works\n* **Get Categories from Supabase**\n* **Download & Transform**\n* **Loop Over Items**\n* **LLM Categorizer** \n* **Insert into Supabase**\n\n---\n\n## How to use\n* Start with the **manual trigger node** or replace it with a schedule/webhook. \n* Connect **Google Drive** to provide Revolut CSV files. \n* Ensure **Supabase** has tables for `transactions` and `categories`. \n* Extend with notifications, reports, or BI tools. \n\n---\n\n## Requirements\n* Google Drive for CSV files \n* Supabase tables for categories & transactions \n* LLM provider (OpenAI/Gemini)"
},
"typeVersion": 1
},
{
"id": "6070fcd4-1cd9-4ffe-a3d3-f7cd582ffc9d",
"name": "Télécharger l'extrait",
"type": "n8n-nodes-base.googleDrive",
"position": [
-1664,
16
],
"parameters": {
"fileId": {
"__rl": true,
"mode": "list",
"value": "1JAb-5Y4Pi2A7LePJ9wvEWVrg6iqN7_5l",
"cachedResultUrl": "https://drive.google.com/file/d/1JAb-5Y4Pi2A7LePJ9wvEWVrg6iqN7_5l/view?usp=drivesdk",
"cachedResultName": "agosto.csv"
},
"options": {},
"operation": "download"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "ehEt6oTyXIIzKB1E",
"name": "Google Drive - kermitdev9"
}
},
"typeVersion": 3
},
{
"id": "01c41ea4-df02-41ec-965f-43a75cb018b4",
"name": "Créer une ligne",
"type": "n8n-nodes-base.supabase",
"onError": "continueErrorOutput",
"position": [
112,
16
],
"parameters": {
"tableId": "transactions",
"fieldsUi": {
"fieldValues": [
{
"fieldId": "completed_date",
"fieldValue": "={{ $json.completed_date }}"
},
{
"fieldId": "started_date",
"fieldValue": "={{ $json.started_date }}"
},
{
"fieldId": "description_original",
"fieldValue": "={{ $json.description_original }}"
},
{
"fieldId": "description_clean",
"fieldValue": "={{ $json.description_clean }}"
},
{
"fieldId": "merchant_name",
"fieldValue": "={{ $json.output.merchant_name }}"
},
{
"fieldId": "category_name",
"fieldValue": "={{ $json.output.category }}"
},
{
"fieldId": "type",
"fieldValue": "={{ $json.type }}"
},
{
"fieldId": "state",
"fieldValue": "={{ $json.state }}"
},
{
"fieldId": "fee",
"fieldValue": "={{ $json.fee }}"
},
{
"fieldId": "currency",
"fieldValue": "={{ $json.currency }}"
},
{
"fieldId": "balance",
"fieldValue": "={{ $json.balance }}"
},
{
"fieldId": "is_subscription",
"fieldValue": "={{ $json.output.is_subscription }}"
},
{
"fieldId": "is_internal",
"fieldValue": "={{ $json.output.is_internal }}"
},
{
"fieldId": "amount",
"fieldValue": "={{ $json.amount }}"
},
{
"fieldId": "uniq_hash",
"fieldValue": "={{ $json.uniq_hash }}"
},
{
"fieldId": "user_id",
"fieldValue": "={{ $json.body?.userId || null }}"
}
]
}
},
"credentials": {
"supabaseApi": {
"id": "bzLzwSBr9xr18RGW",
"name": "Kermitdev9 Supabase"
}
},
"typeVersion": 1
},
{
"id": "45650b46-adeb-4b19-a28e-166ca063f42a",
"name": "Normaliser le contenu",
"type": "n8n-nodes-base.set",
"position": [
-1280,
16
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "42e6f2d4-7530-41ed-95a9-4f8b672e974a",
"name": "type",
"type": "string",
"value": "={{ $json[Object.keys($json)[0]] }}"
},
{
"id": "44447d5c-23b7-470f-982a-b6366035b1eb",
"name": "product",
"type": "string",
"value": "={{ $json[Object.keys($json)[1]].trim() }}"
},
{
"id": "44af0831-d9c6-4ec7-a729-58368b38fbb7",
"name": "started_date",
"type": "string",
"value": "={{ (($json[Object.keys($json)[2]]).replace(\" \", \"T\")) + \"Z\" }}"
},
{
"id": "63e4b1f0-a3b4-438c-b21d-347a8e0702c6",
"name": "completed_date",
"type": "string",
"value": "={{ (( $json[Object.keys($json)[3]]).replace(\" \", \"T\")) + \"Z\" }}"
},
{
"id": "842a5afe-7837-46e0-84cd-a894bbafa58c",
"name": "description_original",
"type": "string",
"value": "={{$json[Object.keys($json)[4]]}}"
},
{
"id": "0cdd36f6-cb48-4512-bc6a-3ce285b09aea",
"name": "description_clean",
"type": "string",
"value": "={{ ($json[Object.keys($json)[4]] || \"\").toLowerCase().replace(/\\s+/g, \" \").trim() }}"
},
{
"id": "9fbfbac5-2d02-4db6-8cb7-9177a66b1e37",
"name": "amount",
"type": "string",
"value": "={{ $json[Object.keys($json)[5]] }}"
},
{
"id": "6a6f1bf9-85ae-47fc-a25e-8c5433e6ac0d",
"name": "fee",
"type": "string",
"value": "={{ $json[Object.keys($json)[6]] }}"
},
{
"id": "8585ec37-33a1-4d4c-9b71-456809ab10da",
"name": "currency",
"type": "string",
"value": "={{ $json[Object.keys($json)[7]].toUpperCase() }}"
},
{
"id": "9b3a2caf-3548-4149-952e-86821f17361b",
"name": "state",
"type": "string",
"value": "={{ $json[Object.keys($json)[8]].toUpperCase() }}"
},
{
"id": "f2a8a409-9f3a-477c-8fe5-0eed9570071c",
"name": "balance",
"type": "string",
"value": "={{ $json[Object.keys($json)[9]] }}"
},
{
"id": "8737fc90-2cdf-4ce1-a793-308ee738bdbf",
"name": "raw",
"type": "string",
"value": "={{ $json }}"
},
{
"id": "c617cc25-10f6-47c0-84c8-06081bfe6672",
"name": "uniq_hash",
"type": "string",
"value": "={{ \n ( $json[Object.keys($json)[3]] || \"\") + \"|\" +\n Number($json[Object.keys($json)[5]]).toFixed(2) + \"|\" +\n ( $json[Object.keys($json)[7]] || \"\").toUpperCase() + \"|\" +\n ( $json[Object.keys($json)[0]] || \"\").toUpperCase()\n}}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "21ec8fe0-b1b6-404e-8e73-0501de474bb8",
"name": "Extraire le marchand",
"type": "n8n-nodes-base.code",
"position": [
-672,
32
],
"parameters": {
"jsCode": "// n8n Function Item\n// Entrada: $json.description_clean (minúsculas, sin espacios dobles)\n// Salida: merchant_candidate, merchant_candidate_normalized\n\nfunction norm(s = \"\") {\n return s\n .toLowerCase()\n .normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/[^a-z0-9\\s]/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nlet desc = String($json.description_clean || \"\").trim();\nif (!desc) return [{ ...$json, merchant_candidate: \"unknown\", merchant_candidate_normalized: \"unknown\" }];\n\n// ruido común\nconst STOP_PHRASES = [\n \"payment from\",\"payment to\",\"transfer from\",\"transfer to\",\"bank transfer\",\n \"card payment\",\"pos purchase\",\"card purchase\",\"transaction at\",\"merchant\",\n \"bill\",\"charge\",\"revolut\",\"sas\",\"sarl\",\"sa\",\"ag\",\"gmbh\",\"spa\",\"srl\",\"ltd\",\n \"limited\",\"inc\",\"corp\",\"co\",\"store\",\"shop\"\n];\n\nconst NOISE_REGEX = [\n /\\b(lu|be|fr|de|nl|es|it|uk|us)\\b/g,\n /\\bcom\\b/g,\n /\\bwww\\b/g,\n /https?:\\/\\/\\S+/g,\n /[0-9]{2,}/g\n];\n\ndesc = norm(desc);\nfor (const rx of NOISE_REGEX) desc = desc.replace(rx, \" \");\nfor (const p of STOP_PHRASES) {\n const rx = new RegExp(`\\\\b${p.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}\\\\b`, \"g\");\n desc = desc.replace(rx, \" \");\n}\ndesc = desc.replace(/\\s+/g, \" \").trim();\n\n// canónicos rápidos\nconst CANONICAL = [\n { re: /\\bamzn|amazon|prime\\s+video|amazon\\s+prime\\b/, name: \"Amazon\" },\n { re: /\\bapple|itunes|apple\\.com\\b/, name: \"Apple\" },\n { re: /\\bspotify\\b/, name: \"Spotify\" },\n { re: /\\bnetflix\\b/, name: \"Netflix\" },\n { re: /\\buber\\b/, name: \"Uber\" },\n { re: /\\bbolt\\b/, name: \"Bolt\" },\n { re: /\\bcarrefour\\b/, name: \"Carrefour\" },\n { re: /\\blidl\\b/, name: \"Lidl\" },\n { re: /\\baldi\\b/, name: \"Aldi\" },\n { re: /\\bdelhaize\\b/, name: \"Delhaize\" }\n];\n\nlet canonical = null;\nfor (const { re, name } of CANONICAL) { if (re.test(desc)) { canonical = name; break; } }\n\n// candidato por primeras palabras relevantes\nconst CONNECTORS = new Set([\"the\",\"and\",\"at\",\"for\",\"from\",\"to\",\"on\",\"in\",\"of\"]);\nlet cand = desc.split(\" \").filter(w => w && !CONNECTORS.has(w)).slice(0, 3).join(\" \").trim();\n\nconst merchant_candidate = canonical || cand || (desc || \"unknown\");\nconst merchant_candidate_normalized = norm(merchant_candidate);\n\nreturn [{\n ...$json,\n merchant_candidate: merchant_candidate_normalized\n}];\n"
},
"typeVersion": 2
},
{
"id": "51b8f681-4242-4692-8873-ea326a3dc312",
"name": "Boucler sur les éléments",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-896,
16
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "48f035d1-2193-4ef1-86bf-9a385ddddadb",
"name": "Extraction de catégorie",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
-496,
-64
],
"parameters": {
"text": "={{ $('Normalize content').item.json.raw }}",
"batching": {},
"messages": {
"messageValues": [
{
"message": "=You are a financial transaction classifier and merchant extractor.\nYou will receive a raw financial transaction in JSON format.\nYou must return a single valid JSON object following exactly the required schema.\n\nAllowed categories:\n{{ $('Aggregate').item.json.normalized_name }}\n\nInstructions:\n1. Read the raw transaction.\n2. Extract a merchant_name: a clean, standardized version of the business or entity involved.\n - If the description clearly contains a brand or business name, return it cleaned (no codes, extra spaces, or special characters).\n - If there is no clear merchant, return \"Unknown\".\n3. Choose exactly ONE category from the allowed list based on the description, merchant, type, and amount.\n4. Set is_internal=true if the movement is between the user’s own accounts (top up, vault, transfer to self).\n5. Set is_subscription=true if the transaction appears to be a recurring service charge (streaming, memberships, etc.).\n6. Set confidence between 0.0 and 1.0 based on how certain you are about the classification.\n7. Set rule_reason as a short, clear explanation of why you chose that category and merchant.\n8. Respond only with JSON. No extra text, no explanations outside the JSON.\n\nExample output:\n{\n \"merchant_name\": \"Spotify\",\n \"category\": \"Subscriptions\",\n \"is_subscription\": true,\n \"is_internal\": false,\n \"confidence\": 0.98,\n \"rule_reason\": \"The description contains 'Spotify', which is a well-known recurring music subscription service.\"\n}\n"
}
]
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.7
},
{
"id": "dbd5bb41-b6df-4f41-a15c-356d55121ccd",
"name": "Fusionner",
"type": "n8n-nodes-base.merge",
"position": [
-144,
16
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineAll"
},
"typeVersion": 3.2
},
{
"id": "41f70e3c-ab40-4695-af06-cc1be6672331",
"name": "Analyseur de sortie",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
-304,
112
],
"parameters": {
"jsonSchemaExample": "{\n \"merchant_name\": \"Spotify\",\n \"category\": \"Subscriptions\",\n \"is_subscription\": true,\n \"is_internal\": false,\n \"confidence\": 0.98,\n \"rule_reason\": \"The description contains 'Spotify', which is a well-known recurring music subscription service.\"\n}"
},
"typeVersion": 1.3
},
{
"id": "fd6174c8-3790-447b-9223-ec575ed9d0c7",
"name": "Obtenir plusieurs lignes",
"type": "n8n-nodes-base.supabase",
"position": [
-2144,
16
],
"parameters": {
"tableId": "categories",
"operation": "getAll",
"returnAll": true
},
"credentials": {
"supabaseApi": {
"id": "bzLzwSBr9xr18RGW",
"name": "Kermitdev9 Supabase"
}
},
"typeVersion": 1
},
{
"id": "4ab626a6-ceb7-4893-b400-5fff770536c1",
"name": "Agréger",
"type": "n8n-nodes-base.aggregate",
"position": [
-1984,
16
],
"parameters": {
"options": {},
"fieldsToAggregate": {
"fieldToAggregate": [
{
"fieldToAggregate": "normalized_name"
}
]
}
},
"typeVersion": 1
},
{
"id": "5e9d2e5d-39ef-4de6-bc7a-fc093ae63a9e",
"name": "Note adhésive1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2192,
-192
],
"parameters": {
"color": 2,
"width": 368,
"height": 464,
"content": "## Get Categories from Supabase\nRetrieve categories from Supabase to feed into later LLM categorization phases."
},
"typeVersion": 1
},
{
"id": "01a5bac3-91ad-4d67-be6c-0379cd2b1ccd",
"name": "Note adhésive2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1776,
-192
],
"parameters": {
"color": 6,
"width": 832,
"height": 464,
"content": "## Download and Transform extraction\nDownload the file from Google Drive, extract and parse the CSV data, normalize and standardize its content, then generate a unique hash to ensure consistency and prevent duplicates."
},
"typeVersion": 1
},
{
"id": "c76a64ba-e9ae-4716-b11e-de03c87ee6ee",
"name": "Note adhésive3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-720,
-192
],
"parameters": {
"color": 2,
"width": 736,
"height": 464,
"content": "## LLM Categorizer\nIterate over all transactions, send each to the LLM together with the previously retrieved category list, and extract the assigned category plus flags for whether it is a subscription, an internal transfer, or other relevant markers."
},
"typeVersion": 1
},
{
"id": "1343af82-305f-488d-9de4-b878eaf95e95",
"name": "Note adhésive4",
"type": "n8n-nodes-base.stickyNote",
"position": [
96,
-192
],
"parameters": {
"color": 2,
"width": 336,
"height": 464,
"content": "## Insert into supabase\nThen insert into supabase avoiding duplicates"
},
"typeVersion": 1
},
{
"id": "af13cecf-f7a9-4ac3-aa81-420de0b26a3f",
"name": "Hachage unique",
"type": "n8n-nodes-base.crypto",
"position": [
-1104,
16
],
"parameters": {
"value": "={{ $json.uniq_hash }}",
"dataPropertyName": "uniq_hash"
},
"typeVersion": 1
},
{
"id": "50638b77-608d-46be-a5fc-11983a3da235",
"name": "gpt-4.1-mini",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-496,
112
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini",
"cachedResultName": "gpt-4.1-mini"
},
"options": {
"temperature": 0.3
}
},
"credentials": {
"openAiApi": {
"id": "3X2EvLOOJbnKaXSS",
"name": "OpenAi account"
}
},
"typeVersion": 1.2
},
{
"id": "5ecee4f9-7527-48ed-bbbb-176c19f5ef84",
"name": "Extraire du fichier",
"type": "n8n-nodes-base.extractFromFile",
"position": [
-1440,
16
],
"parameters": {
"options": {}
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "363d0f72-9959-4964-b2b5-e00ff29f4690",
"connections": {
"dbd5bb41-b6df-4f41-a15c-356d55121ccd": {
"main": [
[
{
"node": "51b8f681-4242-4692-8873-ea326a3dc312",
"type": "main",
"index": 0
},
{
"node": "01c41ea4-df02-41ec-965f-43a75cb018b4",
"type": "main",
"index": 0
}
]
]
},
"4ab626a6-ceb7-4893-b400-5fff770536c1": {
"main": [
[
{
"node": "6070fcd4-1cd9-4ffe-a3d3-f7cd582ffc9d",
"type": "main",
"index": 0
}
]
]
},
"af13cecf-f7a9-4ac3-aa81-420de0b26a3f": {
"main": [
[
{
"node": "51b8f681-4242-4692-8873-ea326a3dc312",
"type": "main",
"index": 0
}
]
]
},
"01c41ea4-df02-41ec-965f-43a75cb018b4": {
"main": [
[],
[]
]
},
"50638b77-608d-46be-a5fc-11983a3da235": {
"ai_languageModel": [
[
{
"node": "48f035d1-2193-4ef1-86bf-9a385ddddadb",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"fd6174c8-3790-447b-9223-ec575ed9d0c7": {
"main": [
[
{
"node": "4ab626a6-ceb7-4893-b400-5fff770536c1",
"type": "main",
"index": 0
}
]
]
},
"41f70e3c-ab40-4695-af06-cc1be6672331": {
"ai_outputParser": [
[
{
"node": "48f035d1-2193-4ef1-86bf-9a385ddddadb",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"51b8f681-4242-4692-8873-ea326a3dc312": {
"main": [
[],
[
{
"node": "21ec8fe0-b1b6-404e-8e73-0501de474bb8",
"type": "main",
"index": 0
}
]
]
},
"6070fcd4-1cd9-4ffe-a3d3-f7cd582ffc9d": {
"main": [
[
{
"node": "5ecee4f9-7527-48ed-bbbb-176c19f5ef84",
"type": "main",
"index": 0
}
]
]
},
"21ec8fe0-b1b6-404e-8e73-0501de474bb8": {
"main": [
[
{
"node": "48f035d1-2193-4ef1-86bf-9a385ddddadb",
"type": "main",
"index": 0
},
{
"node": "dbd5bb41-b6df-4f41-a15c-356d55121ccd",
"type": "main",
"index": 1
}
]
]
},
"5ecee4f9-7527-48ed-bbbb-176c19f5ef84": {
"main": [
[
{
"node": "45650b46-adeb-4b19-a28e-166ca063f42a",
"type": "main",
"index": 0
}
]
]
},
"45650b46-adeb-4b19-a28e-166ca063f42a": {
"main": [
[
{
"node": "af13cecf-f7a9-4ac3-aa81-420de0b26a3f",
"type": "main",
"index": 0
}
]
]
},
"48f035d1-2193-4ef1-86bf-9a385ddddadb": {
"main": [
[
{
"node": "dbd5bb41-b6df-4f41-a15c-356d55121ccd",
"type": "main",
"index": 0
}
]
]
},
"44cf1317-7bac-45f6-a139-6979e1923496": {
"main": [
[
{
"node": "fd6174c8-3790-447b-9223-ec575ed9d0c7",
"type": "main",
"index": 0
}
]
]
}
}
}Comment utiliser ce workflow ?
Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.
Dans quelles scénarios ce workflow est-il adapté ?
Avancé - Création de contenu, IA Multimodale
Est-ce payant ?
Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.
Workflows recommandés
Jose Luis Segura
@jose99seguraPartager ce workflow