{ "meta": { "instanceId": "workflow-03220254", "versionId": "1.0.0", "createdAt": "2025-09-29T07:08:00.950842", "updatedAt": "2025-09-29T07:08:00.950855", "owner": "n8n-user", "license": "MIT", "category": "automation", "status": "active", "priority": "high", "environment": "production" }, "nodes": [ { "id": "c84f3a9a-66b3-4a09-b06a-9b399ea574b8", "name": "OpenAI", "type": "n8n-nodes-base.noOp", "position": [ 420, -240 ], "parameters": { "modelId": { "__rl": true, "mode": "list", "value": "gpt-4.1-mini", "cachedResultName": "GPT-4.1-MINI" }, "options": {}, "messages": { "values": [ { "content": "=Does this PDF file look like a {{ $(\"Configure\").first().json[\"Match on\"] }}? Return \"true\" if it is a {{ $(\"Configure\").first().json[\"Match on\"] }} and \"false\" if not. Only reply with lowercase letters \"true\" or \"false\".\n\nThis is the PDF filename:\n```\n{{ $binary.data.fileName }}\n```\n\nThis is the PDF text content:\n```\n{{ $json.text }}\n```" } ] } }, "credentials": { "openAiApi": { "id": "prYAbsQvWl1pPbdL", "name": "OpenAi account" } }, "typeVersion": 1.8, "notes": "This openAi node performs automated tasks as part of the workflow." }, { "id": "ea1fbc5b-1859-4d65-8401-30baa95fcc52", "name": "Configure", "type": "n8n-nodes-base.set", "position": [ -700, 0 ], "parameters": { "values": { "number": [ { "name": "maxTokenSize", "value": 8000 }, { "name": "replyTokenSize", "value": 50 } ], "string": [ { "name": "Match on", "value": "receipt or invoice that can be considered a software engineering business cost" }, { "name": "Google Drive folder to upload matched PDFs", "value": "{{ $env.WEBHOOK_URL }}[put_folder_id_here]" }, { "name": "sendInvoicesTo" } ], "boolean": [ { "name": "sendEmail", "value": "={{ $('Webhook').item.json.body.sendEmail === \"true\" }}" } ] }, "options": {} }, "typeVersion": 1, "notes": "This set node performs automated tasks as part of the workflow." }, { "id": "3ee63612-c1e7-40e6-a38f-f77f5ee3efa4", "name": "Iterate over email attachments", "type": "n8n-nodes-base.code", "position": [ -200, 0 ], "parameters": { "jsCode": "// {{ $env.WEBHOOK_URL }}\nlet results = [];\n\nfor (const item of $input.all()) {\n console.log(item);\n for (const key of Object.keys(item.binary)) {\n results.push({\n json: {},\n binary: {\n data: item.binary[key],\n }\n });\n }\n}\n\nreturn results;" }, "typeVersion": 1, "notes": "This code node performs automated tasks as part of the workflow." }, { "id": "3e638471-c1c5-4bab-aa2a-12a1777225ec", "name": "Not a PDF", "type": "n8n-nodes-base.noOp", "position": [ 120, 80 ], "parameters": {}, "typeVersion": 1, "notes": "This noOp node performs automated tasks as part of the workflow." }, { "id": "b5af902b-2d59-49ee-b6d8-e387c59b89fd", "name": "Is text within token limit?", "type": "n8n-nodes-base.if", "position": [ 300, -100 ], "parameters": { "conditions": { "boolean": [ { "value1": "={{ $json.text.length() / 4 <= $('Configure').first().json.maxTokenSize - $('Configure').first().json.replyTokenSize }}", "value2": true } ] } }, "typeVersion": 1, "notes": "This if node performs automated tasks as part of the workflow." }, { "id": "a0a8895c-ef8b-44e7-9294-1bcf629d0973", "name": "Merge", "type": "n8n-nodes-base.merge", "position": [ 720, -120 ], "parameters": { "mode": "combine", "options": { "clashHandling": { "values": { "resolveClash": "preferInput1" } } }, "combinationMode": "mergeByPosition" }, "typeVersion": 2, "notes": "This merge node performs automated tasks as part of the workflow." }, { "id": "7565118a-6d44-4583-a19f-cb4177378d33", "name": "Is matched", "type": "n8n-nodes-base.if", "position": [ 880, -120 ], "parameters": { "conditions": { "string": [ { "value1": "={{ $json.message.content }}", "value2": "true" } ] } }, "typeVersion": 1, "notes": "This if node performs automated tasks as part of the workflow." }, { "id": "074ffb7a-f83e-44b8-84fe-7b85f7245bb0", "name": "Upload file to folder", "type": "n8n-nodes-base.googleDrive", "position": [ 1100, -140 ], "parameters": { "name": "={{ $binary.data.fileName }}", "options": {}, "parents": [ "={{ $('Create folder').first().json.id }}" ], "binaryData": true }, "credentials": { "googleDriveOAuth2Api": { "id": "xXHySx4T77sDdTqY", "name": "Google Drive account" } }, "typeVersion": 2, "notes": "This googleDrive node performs automated tasks as part of the workflow." }, { "id": "7681eb62-ba86-4c89-9b88-3ce6fc438bd4", "name": "Webhook", "type": "n8n-nodes-base.webhook", "position": [ -1080, 0 ], "webhookId": "cded3af3-31df-47c2-a826-ff84eb4a41df", "parameters": { "path": "cded3af3-31df-47c2-a826-ff84eb4a41df", "options": {}, "httpMethod": "POST", "responseMode": "responseNode", "authentication": "{{ $credentials.headerAuth }}" }, "credentials": { "httpHeaderAuth": { "id": "90SsOYPPIe3Qv5Rq", "name": "Header Auth account" } }, "typeVersion": 2, "notes": "This webhook node performs automated tasks as part of the workflow." }, { "id": "aab3d940-55c2-40d3-917a-83412d4e378d", "name": "Respond to Webhook", "type": "n8n-nodes-base.respondToWebhook", "position": [ -720, -240 ], "parameters": { "options": { "responseCode": 202 }, "respondWith": "json", "responseBody": "={\n \"status\": \"Accepted\",\n \"driveFolderUrl\": \"{{ \"{{ $env.WEBHOOK_URL }}\" + $json.id }}\"\n}" }, "typeVersion": 1.1, "notes": "This respondToWebhook node performs automated tasks as part of the workflow." }, { "id": "29a4122f-0112-4157-a50d-0a6cf83ab7fd", "name": "Create folder", "type": "n8n-nodes-base.googleDrive", "position": [ -920, 0 ], "parameters": { "name": "={{ \"invoices_\" + $json.body.startDate.split('T')[0] }}", "driveId": { "__rl": true, "mode": "list", "value": "My Drive" }, "options": {}, "folderId": { "__rl": true, "mode": "list", "value": "root", "cachedResultName": "/ (Root folder)" }, "resource": "folder" }, "credentials": { "googleDriveOAuth2Api": { "id": "xXHySx4T77sDdTqY", "name": "Google Drive account" } }, "typeVersion": 3, "notes": "This googleDrive node performs automated tasks as part of the workflow." }, { "id": "df86428f-7e63-4fd9-944c-f48af72af495", "name": "Aggregate attachments", "type": "n8n-nodes-base.code", "position": [ 1200, -340 ], "parameters": { "jsCode": "// \"items\" is the array coming from the previous node (14 items)\nconst merged = { json: {}, binary: {} };\n\nfor (const item of $input.all()) {\n const data = {\n [item.binary.data.fileName]: item.binary.data\n };\n Object.assign(merged.binary, data); // copy every file property\n}\n\nreturn [merged]; // one single item goes out" }, "typeVersion": 2, "notes": "This code node performs automated tasks as part of the workflow." }, { "id": "72a21bfa-6e3b-421a-a4ca-dea9e09a5b0b", "name": "Send email with invoices?", "type": "n8n-nodes-base.if", "position": [ 1000, -320 ], "parameters": { "options": {}, "conditions": { "options": { "version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict" }, "combinator": "and", "conditions": [ { "id": "63caf3d8-39bd-4300-aa7e-8c0ecfc87576", "operator": { "type": "boolean", "operation": "true", "singleValue": true }, "leftValue": "={{ $('Configure').first().json.sendEmail }}", "rightValue": "" } ] } }, "typeVersion": 2.2, "notes": "This if node performs automated tasks as part of the workflow." }, { "id": "bb038635-eb69-447b-a85b-e9c3caebfe3a", "name": "Send to my accountant", "type": "n8n-nodes-base.gmail", "position": [ 1360, -280 ], "webhookId": "3ea4dac1-58fe-4d0e-811b-065ecaef77df", "parameters": { "sendTo": "test@example.com", "message": "Hello, here are my invoices and receipts.", "options": { "attachmentsUi": { "attachmentsBinary": [ { "property": "={{ Object.keys($binary).join(',') }}" } ] } }, "subject": "={{ \n (() => {\n const startDate = $node['Webhook'].json.body.startDate.split('T')[0];\n const endDate = $node['Webhook'].json.body.endDate.split('T')[0];\n return `Dokumenty kosztowe za okres od ${startDate} do ${endDate}`;\n })() \n}}", "emailType": "text" }, "credentials": { "gmailOAuth2": { "id": "PPgHF95PrpAMBlbG", "name": "Gmail account" } }, "typeVersion": 2.1, "notes": "This gmail node performs automated tasks as part of the workflow." }, { "id": "7b2e5c6c-0a95-4347-97a9-c9ffbc0e3af2", "name": "Get emails with attachments", "type": "n8n-nodes-base.gmail", "position": [ -500, 0 ], "webhookId": "6e2ca9f7-6d22-4d94-86bc-8a299bc8e752", "parameters": { "simple": false, "filters": { "q": "has:attachment", "sender": "", "receivedAfter": "={{ $('Webhook').item.json.body.startDate }}", "receivedBefore": "={{ $('Webhook').item.json.body.endDate }}" }, "options": { "downloadAttachments": true, "dataPropertyAttachmentsPrefixName": "attachment_" }, "operation": "getAll", "returnAll": true }, "credentials": { "gmailOAuth2": { "id": "PPgHF95PrpAMBlbG", "name": "Gmail account" } }, "typeVersion": 2.1, "notes": "This gmail node performs automated tasks as part of the workflow." }, { "id": "6d5b2c1b-657d-44bf-980d-fd428fd8d832", "name": "Read PDF email attachments", "type": "n8n-nodes-base.readPDF", "onError": "continueErrorOutput", "position": [ 120, -80 ], "parameters": {}, "notesInFlow": false, "typeVersion": 1, "notes": "This readPDF node performs automated tasks as part of the workflow." }, { "id": "3166f45c-306f-483a-b2c6-6768abc916a0", "name": "Is attachment a PDF?", "type": "n8n-nodes-base.if", "position": [ -40, 0 ], "parameters": { "conditions": { "string": [ { "value1": "={{ $binary.data.fileExtension }}", "value2": "pdf" } ] } }, "typeVersion": 1, "notes": "This if node performs automated tasks as part of the workflow." }, { "id": "866b286a-7b9b-4506-aa6b-d2049b249991", "name": "Optional filter for emails", "type": "n8n-nodes-base.filter", "position": [ -360, 0 ], "parameters": { "options": {}, "conditions": { "options": { "version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict" }, "combinator": "and", "conditions": [ { "id": "687c4cd0-ada5-4dc1-8707-1a9c3b551251", "operator": { "type": "string", "operation": "notEquals" }, "leftValue": "={{ $json.to.value[0].address }}", "rightValue": "" } ] } }, "typeVersion": 2.2, "notes": "This filter node performs automated tasks as part of the workflow." }, { "id": "56133dba-bc93-4f65-be42-995164a45c03", "name": "Sticky Note", "type": "n8n-nodes-base.stickyNote", "position": [ -1600, -340 ], "parameters": { "width": 440, "height": 880, "content": "## Gmail PDF Invoice/Receipt Classifier & Google Drive Uploader (via n8n & OpenAI)\n\n_**DISCLAIMER**: AI classification isn't perfect. Always double-check that the correct documents were identified and uploaded._\n\nThis n8n workflow, triggered via a webhook, scans your Gmail for emails within a specified date range, extracts PDF attachments, and uses OpenAI to determine if each PDF matches a defined category (defaulting to \"receipt or invoice\"). Matched PDFs are then uploaded to a uniquely named Google Drive folder based on the date range. You can customize the classification term (e.g., change \"receipt or invoice\" to \"contract\") and optionally have the workflow email the collected PDFs to a specified address.\n\n### How it works\n1. Triggers via a `Webhook` receiving a start date, end date, and an optional flag to send an email.\n2. Creates a dated folder in `Google Drive` (e.g., `invoices_YYYY-MM-DD_YYYY-MM-DD`).\n3. Fetches emails with attachments from `Gmail` within the specified date range.\n4. Iterates through each attachment, filtering specifically for `PDF` files.\n5. Extracts text from each PDF (skipping if the text exceeds token limits set in the `Configure` node).\n6. Uses the `OpenAI` node to ask if the PDF content and filename look like the item defined in the `Configure` node's \"Match on\" field (e.g., \"receipt or invoice\").\n7. If OpenAI responds with \"true\", the original `PDF` file is uploaded to the `Google Drive` folder created in step 2.\n8. If the initial webhook request included the flag to send an email, it aggregates all successfully matched PDFs and sends them via `Gmail` to the address specified in the `Configure` node.\n\nWorkflow written by [Tom]({{ $env.WEBHOOK_URL }}\n" }, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow." }, { "id": "aa5d8126-e2ec-4476-886d-c46379f1c6e2", "name": "Sticky Note1", "type": "n8n-nodes-base.stickyNote", "position": [ -780, -40 ], "parameters": { "width": 260, "height": 1000, "content": "## Parameters\n\n\n\n\n\n\n\n\n\n* **`maxTokenSize`** (Number)\n * **Limits PDF text length** (estimated input tokens) sent to OpenAI for classification. Prevents errors/high costs on long documents.\n * *Default: 8000*\n\n* **`replyTokenSize`** (Number)\n * **Reserves tokens for OpenAI's reply** ('true'/'false'). Ensures total tokens stay within limits.\n * *Default: 50*\n\n* **`Match on`** (String)\n * **The keyword/phrase OpenAI uses** to identify the desired document type (e.g., \"receipt or invoice\", \"contract\"). Defines what you're searching for.\n * *Default: \"receipt or invoice\"*\n\n* **`sendInvoicesTo`** (String)\n * **Recipient email address** for the final collection of matched PDFs. Used only if `sendEmail` is true.\n * *Example: \"accounting@example.com\"*\n\n* **`sendEmail`** (Boolean)\n * **Turns the final email step on (`true`) or off (`false`)**. Set via the initial webhook trigger. If false, files are only uploaded to Drive.\n * *Example: `true` or `false`*" }, "typeVersion": 1, "notes": "This stickyNote node performs automated tasks as part of the workflow." } ], "pinData": {}, "connections": { "7681eb62-ba86-4c89-9b88-3ce6fc438bd4": { "main": [ [ { "node": "error-handler-7681eb62-ba86-4c89-9b88-3ce6fc438bd4", "type": "main", "index": 0 } ], [ { "node": "error-handler-7681eb62-ba86-4c89-9b88-3ce6fc438bd4-0d70a3b5", "type": "main", "index": 0 } ], [ { "node": "error-handler-7681eb62-ba86-4c89-9b88-3ce6fc438bd4-483aaa04", "type": "main", "index": 0 } ], [ { "node": "error-handler-7681eb62-ba86-4c89-9b88-3ce6fc438bd4-30ef0d84", "type": "main", "index": 0 } ], [ { "node": "error-handler-7681eb62-ba86-4c89-9b88-3ce6fc438bd4-58aecfff", "type": "main", "index": 0 } ], [ { "node": "error-handler-7681eb62-ba86-4c89-9b88-3ce6fc438bd4-03f02ab6", "type": "main", "index": 0 } ], [ { "node": "error-handler-7681eb62-ba86-4c89-9b88-3ce6fc438bd4-5310a9dc", "type": "main", "index": 0 } ], [ { "node": "error-handler-7681eb62-ba86-4c89-9b88-3ce6fc438bd4-596a62d0", "type": "main", "index": 0 } ], [ { "node": "error-handler-7681eb62-ba86-4c89-9b88-3ce6fc438bd4-8da490e7", "type": "main", "index": 0 } ] ] }, "aab3d940-55c2-40d3-917a-83412d4e378d": { "main": [ [ { "node": "error-handler-aab3d940-55c2-40d3-917a-83412d4e378d", "type": "main", "index": 0 } ], [ { "node": "error-handler-aab3d940-55c2-40d3-917a-83412d4e378d-6d67142f", "type": "main", "index": 0 } ], [ { "node": "error-handler-aab3d940-55c2-40d3-917a-83412d4e378d-f51a43a5", "type": "main", "index": 0 } ], [ { "node": "error-handler-aab3d940-55c2-40d3-917a-83412d4e378d-882c5603", "type": "main", "index": 0 } ], [ { "node": "error-handler-aab3d940-55c2-40d3-917a-83412d4e378d-911340ba", "type": "main", "index": 0 } ], [ { "node": "error-handler-aab3d940-55c2-40d3-917a-83412d4e378d-cbaf16d4", "type": "main", "index": 0 } ], [ { "node": "error-handler-aab3d940-55c2-40d3-917a-83412d4e378d-e937084a", "type": "main", "index": 0 } ], [ { "node": "error-handler-aab3d940-55c2-40d3-917a-83412d4e378d-e61b391c", "type": "main", "index": 0 } ], [ { "node": "error-handler-aab3d940-55c2-40d3-917a-83412d4e378d-247bf0be", "type": "main", "index": 0 } ] ] }, "c84f3a9a-66b3-4a09-b06a-9b399ea574b8": { "main": [ [ { "node": "error-handler-c84f3a9a-66b3-4a09-b06a-9b399ea574b8-fa2828d9", "type": "main", "index": 0 } ] ] }, "074ffb7a-f83e-44b8-84fe-7b85f7245bb0": { "main": [ [ { "node": "error-handler-074ffb7a-f83e-44b8-84fe-7b85f7245bb0-bf39ab0f", "type": "main", "index": 0 } ] ] }, "29a4122f-0112-4157-a50d-0a6cf83ab7fd": { "main": [ [ { "node": "error-handler-29a4122f-0112-4157-a50d-0a6cf83ab7fd-af6fbc58", "type": "main", "index": 0 } ] ] } }, "name": "Openai Workflow", "settings": { "executionOrder": "v1", "saveManualExecutions": true, "callerPolicy": "workflowsFromSameOwner", "errorWorkflow": null, "timezone": "UTC", "executionTimeout": 3600, "maxExecutions": 1000, "retryOnFail": true, "retryCount": 3, "retryDelay": 1000 }, "description": "Automated workflow: Openai Workflow. This workflow integrates 14 different services: webhook, filter, stickyNote, code, readPDF. It contains 27 nodes and follows best practices for error handling and security.", "tags": [ "automation", "n8n", "production-ready", "excellent", "optimized" ], "notes": "Excellent quality workflow: Openai Workflow. This workflow has been optimized for production use with comprehensive error handling, security, and documentation." }