{ "name": "Folderit — Email attachment archive with durable receipts", "nodes": [ { "id": "80496824-1baa-5330-84ab-6c03aea58e6e", "name": "New email with attachments", "type": "n8n-nodes-base.emailReadImap", "typeVersion": 2.2, "position": [ 0, 400 ], "parameters": { "mailbox": "INBOX", "postProcessAction": "nothing", "downloadAttachments": true, "format": "simple", "dataPropertyAttachmentsPrefixName": "attachment_", "options": { "trackLastMessageId": true } } }, { "id": "aaff4754-021a-5747-b23b-725e44c7e6e7", "name": "Settings", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 240, 400 ], "parameters": { "mode": "raw", "jsonOutput": "{\n \"accountUid\": \"YOUR_ACCOUNT_UID\",\n \"folderUid\": \"YOUR_DESTINATION_FOLDER_UID\",\n \"ledgerTableId\": \"YOUR_LEDGER_TABLE_ID\",\n \"mailboxScope\": \"YOUR_STABLE_MAILBOX_LABEL\",\n \"allowedExtensions\": \"pdf,docx,txt\"\n}", "options": {} } }, { "id": "524f913d-0ae4-57db-a5b1-fbd9ca0008b6", "name": "Split and validate attachments", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 480, 400 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const cfg=$('Settings').first().json;\nfor(const key of ['accountUid','folderUid','ledgerTableId','mailboxScope']) if(typeof cfg[key]!=='string'||!cfg[key].trim()||cfg[key].startsWith('YOUR_')) throw new Error(`Replace ${key} in Settings.`);\nconst allowed=String(cfg.allowedExtensions||'').split(',').map(x=>x.trim().toLowerCase().replace(/^\\./,'')).filter(Boolean);\nif(!allowed.length)throw new Error('Set allowedExtensions, for example pdf,docx,txt.');\nconst out=[]; const mail=$('New email with attachments').all();\nfor(let i=0;ik.startsWith('attachment_'));\n if(parts.length&&!messageId)throw new Error('Email has no stable Message-ID/provider ID. Inspect it before archiving.');\n for(const [field,b] of parts){\n const fileName=String(b.fileName||'').split(/[\\\\/]/).pop().replace(/[\\x00-\\x1f\\x7f]/g,'').trim();\n const ext=fileName.includes('.')?fileName.split('.').pop().toLowerCase():'';\n if(!fileName||!allowed.includes(ext))continue;\n out.push({json:{accountUid:cfg.accountUid,folderUid:cfg.folderUid,mailboxScope:cfg.mailboxScope,messageId,sourceAttachment:field,fileName},binary:{data:{...b,fileName}},pairedItem:{item:i}});\n }\n}\nreturn out;" } }, { "id": "b613c6dc-9441-52d8-9e35-f563dd9b3ff5", "name": "Hash attachment bytes", "type": "n8n-nodes-base.crypto", "typeVersion": 2, "position": [ 720, 400 ], "parameters": { "action": "hash", "binaryData": true, "binaryPropertyName": "data", "type": "SHA256", "dataPropertyName": "payloadHash", "encoding": "hex" } }, { "id": "54ec7af5-1323-5b28-b107-b34023730e8a", "name": "Build attachment receipt keys", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 900, 400 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const attachments=$('Split and validate attachments').all();\nreturn $input.all().map((x,i)=>{\n if(!/^[a-f0-9]{64}$/.test(x.json.payloadHash||''))throw new Error('Missing attachment SHA-256.');\n if(x.json.payloadHash==='e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')throw new Error('Empty attachments cannot be archived.');\n const binary=attachments[i]?.binary;\n if(!binary?.data)throw new Error('The source attachment binary is unavailable; no upload was attempted.');\n const j=x.json;\n return {json:{...j,receiptKey:JSON.stringify(['archive-v1',j.accountUid,j.folderUid,j.mailboxScope,j.messageId,j.sourceAttachment,j.fileName,j.payloadHash]),sourceRef:JSON.stringify([j.mailboxScope,j.messageId,j.sourceAttachment])},binary,pairedItem:{item:i}};\n});" } }, { "id": "f31381ef-b3bc-56d9-b93a-0bb7ad339c63", "name": "Process one item", "type": "n8n-nodes-base.splitInBatches", "typeVersion": 3, "position": [ 1020, 760 ], "parameters": { "batchSize": 1, "options": {} } }, { "id": "b375a68c-57ab-5a3e-be81-5b0a0b061b38", "name": "Read processing lock", "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ 1020, 400 ], "parameters": { "resource": "row", "operation": "get", "dataTableId": { "__rl": true, "mode": "id", "value": "={{ $('Settings').first().json.ledgerTableId }}" }, "matchType": "allConditions", "filters": { "conditions": [ { "keyName": "key", "condition": "eq", "keyValue": "__mutex__" } ] }, "returnAll": false, "limit": 2 }, "alwaysOutputData": true }, { "id": "5dfa307e-0280-5ea8-91d6-a600aa9db636", "name": "Check lock record", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1260, 400 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "if (!$execution.id) throw new Error('No execution identity is available; processing was stopped before locking.');\nconst rows = $input.all().filter(x => x.json.id !== undefined);\nif (rows.length !== 1 || rows[0].json.sourceRef !== 'folderit-ledger/v1') throw new Error('Ledger lock is missing, duplicated or has the wrong schema. Run the setup workflow once and inspect the table.');\nreturn rows;" } }, { "id": "a61c54c5-6ae3-51ca-ae52-9a58de473439", "name": "Acquire processing lock", "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ 1500, 400 ], "parameters": { "resource": "row", "operation": "update", "dataTableId": { "__rl": true, "mode": "id", "value": "={{ $('Settings').first().json.ledgerTableId }}" }, "matchType": "allConditions", "filters": { "conditions": [ { "keyName": "id", "condition": "eq", "keyValue": "={{ $json.id }}" }, { "keyName": "key", "condition": "eq", "keyValue": "__mutex__" }, { "keyName": "state", "condition": "eq", "keyValue": "free" }, { "keyName": "owner", "condition": "eq", "keyValue": "" } ] }, "columns": { "mappingMode": "defineBelow", "value": { "state": "held", "owner": "={{ String($execution.id) }}" }, "matchingColumns": [], "schema": [ { "id": "key", "displayName": "key", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "state", "displayName": "state", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "owner", "displayName": "owner", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "payloadHash", "displayName": "payloadHash", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "targetUid", "displayName": "targetUid", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "sourceRef", "displayName": "sourceRef", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false } ] }, "options": {} }, "alwaysOutputData": true }, { "id": "88a5f52b-088b-5380-9b1c-6c183450a123", "name": "Confirm lock ownership", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1740, 400 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const rows = $input.all().filter(x => x.json.id !== undefined);\nif (rows.length !== 1 || rows[0].json.owner !== String($execution.id) || rows[0].json.state !== 'held') throw new Error('Another run owns the processing lock. No destination write was attempted by this run. Retain this failed input and retry after the owner completes.');\nreturn rows;" } }, { "id": "8ff0cf1d-a760-526e-b3b9-c70e4fe2419c", "name": "Find processing receipt", "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ 1980, 400 ], "parameters": { "resource": "row", "operation": "get", "dataTableId": { "__rl": true, "mode": "id", "value": "={{ $('Settings').first().json.ledgerTableId }}" }, "matchType": "allConditions", "filters": { "conditions": [ { "keyName": "key", "condition": "eq", "keyValue": "={{ $('Process one item').item.json.receiptKey }}" } ] }, "returnAll": false, "limit": 2 }, "alwaysOutputData": true }, { "id": "4d153505-7c73-5c52-80a3-4f6894d745e4", "name": "Decide whether to process", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2220, 400 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const item = $('Process one item').item;\nconst rows = $input.all().filter(x => x.json.id !== undefined);\nif (rows.length > 1) throw new Error('Duplicate ledger keys need reconciliation. No destination write was attempted.');\nif (rows.length === 1) {\n const receipt = rows[0].json;\n if (receipt.key !== item.json.receiptKey || (item.json.payloadHash && receipt.payloadHash !== item.json.payloadHash)) throw new Error('The receipt does not match this work item. Inspect before continuing.');\n if (receipt.state !== 'done' || !receipt.targetUid) throw new Error('A prior attempt is unresolved. Inspect the receipt, its owner execution and the destination before clearing the processing lock. Do not repeat the write.');\n return [{json: {...item.json, route: 'done', targetUid: receipt.targetUid, receiptRowId: receipt.id}, binary: item.binary, pairedItem: {item: 0}}];\n}\nreturn [{json: {...item.json, route: 'new'}, binary: item.binary, pairedItem: {item: 0}}];" } }, { "id": "c7bf7b9f-f7d5-536c-9abe-70016fdcbced", "name": "Already completed?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 2460, 400 ], "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "bd514677-bf15-5a33-9398-5564d7f24e27", "leftValue": "={{ $json.route }}", "rightValue": "done", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} } }, { "id": "eab8f60f-4e1a-5a44-a40f-0dcaa429a572", "name": "Record pending attempt", "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ 2700, 540 ], "parameters": { "resource": "row", "operation": "insert", "dataTableId": { "__rl": true, "mode": "id", "value": "={{ $('Settings').first().json.ledgerTableId }}" }, "matchType": "allConditions", "filters": { "conditions": [] }, "columns": { "mappingMode": "defineBelow", "value": { "key": "={{ $json.receiptKey }}", "state": "pending", "owner": "={{ String($execution.id) }}", "payloadHash": "={{ $json.payloadHash || \"\" }}", "targetUid": "", "sourceRef": "={{ $json.sourceRef }}" }, "matchingColumns": [], "schema": [ { "id": "key", "displayName": "key", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "state", "displayName": "state", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "owner", "displayName": "owner", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "payloadHash", "displayName": "payloadHash", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "targetUid", "displayName": "targetUid", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "sourceRef", "displayName": "sourceRef", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false } ] }, "options": {} }, "alwaysOutputData": true }, { "id": "8d1259b9-2d00-56a4-a9af-f172f5be327d", "name": "Restore work item", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2940, 540 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const rows = $input.all().filter(x => x.json.id !== undefined);\nconst source = $('Process one item').item;\nif (rows.length !== 1 || rows[0].json.state !== 'pending' || rows[0].json.owner !== String($execution.id) || rows[0].json.key !== source.json.receiptKey) throw new Error('Pending receipt was not saved as expected. No destination write was attempted.');\nreturn [{json: {...source.json, receiptRowId: rows[0].json.id}, binary: source.binary, pairedItem: {item: 0}}];" } }, { "id": "c08a4d56-4194-5c0e-b94c-d80c3d079878", "name": "Upload attachment", "type": "n8n-nodes-folderit.folderit", "typeVersion": 1, "position": [ 3180, 540 ], "parameters": { "resource": "file", "operation": "upload", "accountUid": "={{ $json.accountUid }}", "folderUid": "={{ $json.folderUid }}", "binaryPropertyName": "data" } }, { "id": "0c3bdfe5-e87b-59cb-85f2-6a91cc468fb9", "name": "Read uploaded document", "type": "n8n-nodes-folderit.folderit", "typeVersion": 1, "position": [ 3420, 540 ], "parameters": { "resource": "file", "operation": "get", "accountUid": "={{ $('Settings').first().json.accountUid }}", "fileUid": "={{ $json.uid }}" } }, { "id": "d3dc1007-63b6-5f1d-ad49-e410f7980bd1", "name": "Download stored attachment", "type": "n8n-nodes-folderit.folderit", "typeVersion": 1, "position": [ 3540, 760 ], "parameters": { "resource": "file", "operation": "download", "accountUid": "={{ $('Settings').first().json.accountUid }}", "fileUid": "={{ $json.uid }}", "outputBinaryField": "verifiedFile" } }, { "id": "defac51e-6625-5964-927e-24268fbedb7d", "name": "Hash stored attachment bytes", "type": "n8n-nodes-base.crypto", "typeVersion": 2, "position": [ 3780, 760 ], "parameters": { "action": "hash", "binaryData": true, "binaryPropertyName": "verifiedFile", "type": "SHA256", "dataPropertyName": "verifiedHash", "encoding": "hex" } }, { "id": "1ae43e33-0566-5a2e-a172-55eb82b3cb38", "name": "Verify destination result", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 3660, 540 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const expected=$('Restore work item').item.json; const actual=$('Read uploaded document').item.json; const verifiedHash=$input.first().json.verifiedHash;\nif(!actual.uid || actual.name!==expected.fileName || actual.folderUid!==expected.folderUid || verifiedHash!==expected.payloadHash) throw new Error('Uploaded file readback/hash did not match. Keep the pending receipt and inspect the destination before retrying.');\nreturn [{json:{receiptRowId:expected.receiptRowId,targetUid:actual.uid},pairedItem:{item:0}}];" } }, { "id": "39c85e7c-5795-5f60-aa92-8081d18e6c2b", "name": "Save completed receipt", "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ 3900, 540 ], "parameters": { "resource": "row", "operation": "update", "dataTableId": { "__rl": true, "mode": "id", "value": "={{ $('Settings').first().json.ledgerTableId }}" }, "matchType": "allConditions", "filters": { "conditions": [ { "keyName": "id", "condition": "eq", "keyValue": "={{ $json.receiptRowId }}" }, { "keyName": "state", "condition": "eq", "keyValue": "pending" }, { "keyName": "owner", "condition": "eq", "keyValue": "={{ String($execution.id) }}" } ] }, "columns": { "mappingMode": "defineBelow", "value": { "state": "done", "targetUid": "={{ $json.targetUid }}" }, "matchingColumns": [], "schema": [ { "id": "key", "displayName": "key", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "state", "displayName": "state", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "owner", "displayName": "owner", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "payloadHash", "displayName": "payloadHash", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "targetUid", "displayName": "targetUid", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "sourceRef", "displayName": "sourceRef", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false } ] }, "options": {} }, "alwaysOutputData": true }, { "id": "5ebbf1ca-e785-5f12-8ec1-33b9eb4b92b9", "name": "Confirm completed receipt", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 4140, 540 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const rows = $input.all().filter(x => x.json.id !== undefined);\nconst expected = $('Verify destination result').item.json;\nif (rows.length !== 1 || rows[0].json.state !== 'done' || rows[0].json.targetUid !== expected.targetUid || rows[0].json.owner !== String($execution.id)) throw new Error('The destination action succeeded but its receipt could not be confirmed. Inspect before retrying.');\nreturn [{json: {status: 'completed', targetUid: rows[0].json.targetUid, receiptKey: rows[0].json.key}, pairedItem: {item: 0}}];" } }, { "id": "a8034dd6-f586-5f6a-9841-432c8a8d3faf", "name": "Report previous completion", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2700, 200 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "return [{json: {status: 'previously_completed', targetUid: $json.targetUid, receiptKey: $json.receiptKey}}];" } }, { "id": "4977e50e-4982-5b33-9ce4-8aedb7b0463d", "name": "Release processing lock", "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ 4380, 400 ], "parameters": { "resource": "row", "operation": "update", "dataTableId": { "__rl": true, "mode": "id", "value": "={{ $('Settings').first().json.ledgerTableId }}" }, "matchType": "allConditions", "filters": { "conditions": [ { "keyName": "key", "condition": "eq", "keyValue": "__mutex__" }, { "keyName": "state", "condition": "eq", "keyValue": "held" }, { "keyName": "owner", "condition": "eq", "keyValue": "={{ String($execution.id) }}" } ] }, "columns": { "mappingMode": "defineBelow", "value": { "state": "free", "owner": "" }, "matchingColumns": [], "schema": [ { "id": "key", "displayName": "key", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "state", "displayName": "state", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "owner", "displayName": "owner", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "payloadHash", "displayName": "payloadHash", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "targetUid", "displayName": "targetUid", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "sourceRef", "displayName": "sourceRef", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false } ] }, "options": {} }, "alwaysOutputData": true }, { "id": "1c93b2a8-cefc-5f26-8be4-e99073263582", "name": "Confirm lock released", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 4620, 400 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const rows = $input.all().filter(x => x.json.id !== undefined);\nif (rows.length !== 1 || rows[0].json.state !== 'free' || rows[0].json.owner !== '' || rows[0].json.key !== '__mutex__') throw new Error('Could not confirm processing lock release. Inspect the table and owner execution.');\nreturn [{json: {lockReleased: true}}];" } }, { "id": "96a8c7f0-81fd-5a9c-86b3-049e276393b1", "name": "Lock acquired?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 1620, 120 ], "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "25b1f340-09d9-5114-817b-5d4c95b55a4b", "leftValue": "={{ ($json.owner === String($execution.id) && $json.state === 'held') ? 'yes' : 'no' }}", "rightValue": "yes", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} } }, { "id": "342de397-e972-5ee2-a7ef-49926118fa59", "name": "Limit lock waiting", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1800, 100 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "if ($runIndex >= 11) throw new Error('Processing lock is still busy after bounded waiting. No destination action was repeated. Retain this failed input and inspect the owner execution.'); return [{json:{waitingForLock:true},pairedItem:{item:0}}];" } }, { "id": "8c8c8891-d811-5b5b-b4d0-278be5270b17", "name": "Wait before checking lock", "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ 2000, 100 ], "parameters": { "amount": 5, "unit": "seconds" } }, { "id": "290218c1-7eee-5f0d-9a73-70038714912b", "name": "Guide: Archive attachments with saved receipts", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 0, -170 ], "parameters": { "content": "# Archive attachments with saved receipts\nFor **Folderit Tailor-plan users** with API credentials. Requires an n8n release with Data Table node v1.1; tested runtime: n8n 2.42.5. [Install and connect Folderit](https://www.folderit.com/knowledge-base/connect-folderit-to-n8n/)\n\nRun **folderit-ledger-setup.json** once in the same n8n project. Copy its table ID into Settings. All copies processing the same source/destination must use the SAME table. Choose Folderit credentials in every Folderit node. Never put secrets in Settings.\n\nImport **folderit-failure-alert-email.json**, configure its SMTP connection, save it, then select it in this workflow's **⋯ → Settings → Error workflow**. Error workflows must be tested using an activated workflow; manual runs do not trigger them.\n\nConnect your IMAP credential in the trigger. Use a dedicated intake mailbox with TLS; existing unseen messages can be fetched on the first run. Read/unread status is unchanged. Messages without a stable Message-ID are held for inspection.\n\nSet all Settings placeholders, then select the same Folderit credential in Upload attachment, Read uploaded document and Download stored attachment. mailboxScope must remain a stable label for this mailbox. Configure allowedExtensions. Multiple permitted attachments are processed one at a time. Test a replay and confirm no second file appears.", "width": 1100, "height": 650 } }, { "id": "1b6b3a5e-32fe-59a3-a904-2e1ed90afa14", "name": "Guide: Recovery and limits", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 1180, -170 ], "parameters": { "content": "# Recovery and limits\nThe table contains a single processing lock plus durable receipts. Only a run owning the lock may perform an upload or send a message. A completed receipt suppresses repeats for its key. The lock has NO automatic expiry: a crash, failed readback or uncertain API response keeps work blocked for inspection.\n\nCompeting runs wait and retry lock acquisition for about one minute; uploads/messages are never automatically retried. If a run still reports a busy lock, retry that FAILED execution after the owner finishes. Do not discard its input. On other failures, inspect saved executions, receipt state and the actual destination. Never clear the lock while its owner may still be running. Reconcile pending rows before clearing it. See CUSTOMER-WORKFLOWS.md for recovery steps.\n\nThis is duplicate protection with explicit recovery, not an exactly-once guarantee across n8n and an external service. Keep failed execution/binary data long enough for recovery and monitor the failure-alert workflow itself. Production provider delivery and fresh Cloud behavior require validation in your environment.\n\nOne receipt key includes account, destination, mailbox, message ID, attachment field, filename and SHA-256. A new email can intentionally archive identical content. Every new upload is downloaded again and its bytes hashed before completion is recorded; this adds download traffic and avoids depending on asynchronously populated server hash metadata. Empty files and files over 100 MiB fail. Extension checks do not replace malware scanning. No automatic upload retries.", "width": 1050, "height": 650 } } ], "connections": { "New email with attachments": { "main": [ [ { "node": "Settings", "type": "main", "index": 0 } ] ] }, "Settings": { "main": [ [ { "node": "Split and validate attachments", "type": "main", "index": 0 } ] ] }, "Split and validate attachments": { "main": [ [ { "node": "Hash attachment bytes", "type": "main", "index": 0 } ] ] }, "Hash attachment bytes": { "main": [ [ { "node": "Build attachment receipt keys", "type": "main", "index": 0 } ] ] }, "Build attachment receipt keys": { "main": [ [ { "node": "Process one item", "type": "main", "index": 0 } ] ] }, "Process one item": { "main": [ [], [ { "node": "Read processing lock", "type": "main", "index": 0 } ] ] }, "Read processing lock": { "main": [ [ { "node": "Check lock record", "type": "main", "index": 0 } ] ] }, "Check lock record": { "main": [ [ { "node": "Acquire processing lock", "type": "main", "index": 0 } ] ] }, "Acquire processing lock": { "main": [ [ { "node": "Lock acquired?", "type": "main", "index": 0 } ] ] }, "Confirm lock ownership": { "main": [ [ { "node": "Find processing receipt", "type": "main", "index": 0 } ] ] }, "Find processing receipt": { "main": [ [ { "node": "Decide whether to process", "type": "main", "index": 0 } ] ] }, "Decide whether to process": { "main": [ [ { "node": "Already completed?", "type": "main", "index": 0 } ] ] }, "Lock acquired?": { "main": [ [ { "node": "Confirm lock ownership", "type": "main", "index": 0 } ], [ { "node": "Limit lock waiting", "type": "main", "index": 0 } ] ] }, "Limit lock waiting": { "main": [ [ { "node": "Wait before checking lock", "type": "main", "index": 0 } ] ] }, "Wait before checking lock": { "main": [ [ { "node": "Read processing lock", "type": "main", "index": 0 } ] ] }, "Already completed?": { "main": [ [ { "node": "Report previous completion", "type": "main", "index": 0 } ], [ { "node": "Record pending attempt", "type": "main", "index": 0 } ] ] }, "Record pending attempt": { "main": [ [ { "node": "Restore work item", "type": "main", "index": 0 } ] ] }, "Restore work item": { "main": [ [ { "node": "Upload attachment", "type": "main", "index": 0 } ] ] }, "Upload attachment": { "main": [ [ { "node": "Read uploaded document", "type": "main", "index": 0 } ] ] }, "Read uploaded document": { "main": [ [ { "node": "Download stored attachment", "type": "main", "index": 0 } ] ] }, "Download stored attachment": { "main": [ [ { "node": "Hash stored attachment bytes", "type": "main", "index": 0 } ] ] }, "Hash stored attachment bytes": { "main": [ [ { "node": "Verify destination result", "type": "main", "index": 0 } ] ] }, "Verify destination result": { "main": [ [ { "node": "Save completed receipt", "type": "main", "index": 0 } ] ] }, "Save completed receipt": { "main": [ [ { "node": "Confirm completed receipt", "type": "main", "index": 0 } ] ] }, "Confirm completed receipt": { "main": [ [ { "node": "Release processing lock", "type": "main", "index": 0 } ] ] }, "Release processing lock": { "main": [ [ { "node": "Confirm lock released", "type": "main", "index": 0 } ] ] }, "Report previous completion": { "main": [ [ { "node": "Release processing lock", "type": "main", "index": 0 } ] ] }, "Confirm lock released": { "main": [ [ { "node": "Process one item", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1", "saveDataErrorExecution": "all", "saveDataSuccessExecution": "all", "saveExecutionProgress": true }, "pinData": {} }