Build pack: Inbox sorter build pack
Everything from the video: the prompt that sorts each email, the importable n8n workflow with all three piles, and the setup checklist.
Watch the build: full video · 60-second version
- prompt
The sorting prompt
Goes in 'Prompt for Claude'. Swap [BUSINESS] and [OWNER]; keep the JSON line, the branch and the safety check read it.
[BUSINESS]'s inbox got an email. From: {{ $json.name }}. Subject: "{{ $json.subject }}". Email: "{{ $json.message }}" Dollar amounts in it: {{ $json.amounts || 'none' }}. Sort it for [OWNER], the owner. A customer, a job, a question or a complaint = route customer: say why, draft [OWNER]'s reply. A bill or invoice [OWNER] has to pay = route bill: copy the amount and due date exactly as written. Newsletters, promos, receipts, notifications = route later. Not sure = customer. Never invent an amount. Answer as JSON: {"route": "customer, bill or later", "why": "under 10 words", "amount": "as written, or none", "due": "as written, or none", "draft": "customer only: [OWNER]'s reply, under 60 words, signed [OWNER], [BUSINESS], or none"} - json
n8n workflow (import this)
Credentials, sheet IDs and keys removed; reconnect your Gmail and Google Sheets and set your own alert address.
{ "name": "Inbox sorter (customers go to the owner with a reply drafted, bills filed with amount + due date, junk to read later)", "nodes": [ { "name": "New email", "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.2, "position": [ 0, 0 ], "parameters": { "formTitle": "[BUSINESS]", "formDescription": "Send us an email.", "formFields": { "values": [ { "fieldLabel": "Name", "requiredField": true }, { "fieldLabel": "Email", "fieldType": "email", "requiredField": true }, { "fieldLabel": "Subject", "requiredField": true }, { "fieldLabel": "Message", "fieldType": "textarea", "requiredField": true } ] }, "options": { "appendAttribution": false, "path": "new-email", "respondWithOptions": { "values": { "formSubmittedText": "Sent." } } } } }, { "name": "Find the money", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 0, 224 ], "parameters": { "mode": "runOnceForAllItems", "language": "javaScript", "jsCode": "const f = $('New email').first().json;\nconst message = String(f['Message'] || '').trim();\nconst amounts = [...new Set(message.match(/\\$\\s?\\d[\\d,]*(?:\\.\\d{2})?/g) || [])].map(a => a.replace(/\\s/g, ''));\nreturn [{ json: { name: String(f['Name'] || '').trim(), email: String(f['Email'] || '').trim(),\n subject: String(f['Subject'] || '').trim(), message, amounts: amounts.join(', ') } }];" } }, { "name": "Prompt for Claude", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 0, 448 ], "parameters": { "assignments": { "assignments": [ { "id": "p1", "name": "prompt", "type": "string", "value": "=[BUSINESS]'s inbox got an email. From: {{ $json.name }}. Subject: \"{{ $json.subject }}\". Email: \"{{ $json.message }}\" Dollar amounts in it: {{ $json.amounts || 'none' }}. Sort it for [OWNER], the owner. A customer, a job, a question or a complaint = route customer: say why, draft [OWNER]'s reply. A bill or invoice [OWNER] has to pay = route bill: copy the amount and due date exactly as written. Newsletters, promos, receipts, notifications = route later. Not sure = customer. Never invent an amount. Answer as JSON: {\"route\": \"customer, bill or later\", \"why\": \"under 10 words\", \"amount\": \"as written, or none\", \"due\": \"as written, or none\", \"draft\": \"customer only: [OWNER]'s reply, under 60 words, signed [OWNER], [BUSINESS], or none\"}" }, { "id": "p2", "name": "name", "type": "string", "value": "={{ $json.name }}" }, { "id": "p3", "name": "subject", "type": "string", "value": "={{ $json.subject }}" }, { "id": "p4", "name": "amounts", "type": "string", "value": "={{ $json.amounts }}" }, { "id": "p5", "name": "gotAt", "type": "string", "value": "={{ $now.setZone('America/New_York').toFormat('h:mm:ss a') }}" } ] }, "options": {} } }, { "name": "Queue for Claude", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 0, 672 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Queue!A:E:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleDriveOAuth2Api", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ values: [[ $json.gotAt, $json.name, $json.prompt, $execution.resumeUrl, '' ]] }) }}", "options": {} } }, { "name": "Claude sorts it", "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ 0, 896 ], "parameters": { "resume": "webhook", "httpMethod": "POST", "limitWaitTime": true, "resumeAmount": 5, "resumeUnit": "minutes", "options": {} } }, { "name": "Check the reply", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 0, 1120 ], "parameters": { "mode": "runOnceForAllItems", "language": "javaScript", "jsCode": "const KEY = \"<<DEMO_RESUME_KEY>>\";\nconst it = $('Claude sorts it').first().json;\nconst h = it.headers || {};\nif ((h['x-demo-key'] || '') !== KEY) throw new Error('resume rejected: missing or wrong key');\nconst b = it.body || {};\nconst out = {};\nout[\"why\"] = String(b[\"why\"] ?? '').slice(0, 120);\nout[\"amount\"] = String(b[\"amount\"] ?? '').slice(0, 24);\nout[\"due\"] = String(b[\"due\"] ?? '').slice(0, 40);\nout[\"draft\"] = String(b[\"draft\"] ?? '').slice(0, 600);\nconst MOODS = [\"customer\", \"bill\", \"later\"];\nconst m = String(b.route ?? '').trim().toLowerCase();\nout.route = MOODS.includes(m) ? m : \"customer\";\nconst s = $('Find the money').first().json;\nconst num = x => String(x || '').replace(/[$,\\s]/g, '');\nconst found = s.amounts.split(', ').filter(Boolean).map(num);\nif (out.route === 'bill' && !found.includes(num(out.amount))) { out.route = 'customer'; out.why = 'a bill, but the amount is not in the email'; }\nconst due = out.due.trim().toLowerCase();\nif (out.route === 'bill' && (!due || due === 'none' || !s.message.toLowerCase().includes(due))) { out.route = 'customer'; out.why = 'a bill, but no due date in the email'; }\nif (out.route === 'later' && /urgent|asap|emergency|flood|leak/i.test(s.subject + ' ' + s.message)) { out.route = 'customer'; out.why = 'sorted as junk but says urgent'; }\nif (!out.why || out.why.toLowerCase() === 'none') out.why = 'it needs you';\nif (!out.draft || out.draft.toLowerCase() === 'none') out.draft = 'no draft, read their email';\nconst ALERT_TO = 'YOUR_ALERT_EMAIL'; // your own address: the only one this workflow ever emails\nconst TEST_MODE = true; // set false only after the trigger is protected (setup checklist)\nconst TEST_ADDRESSES = ['YOUR_TEST_EMAIL'];\nconst to = ALERT_TO.trim().toLowerCase();\nif (out.route === 'customer') {\n if (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(to)) throw new Error('send blocked: bad address');\n if (TEST_MODE && !TEST_ADDRESSES.map(a => a.toLowerCase()).includes(to)) throw new Error('send blocked: TEST_MODE is on and this is not one of your test addresses');\n}\nout.to = to;\nreturn [{ json: { body: out } }];" } }, { "name": "Which pile?", "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [ 0, 1344 ], "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "r_customer", "leftValue": "={{ $json.body.route }}", "rightValue": "customer", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Customer" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "r_bill", "leftValue": "={{ $json.body.route }}", "rightValue": "bill", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Bill" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "r_later", "leftValue": "={{ $json.body.route }}", "rightValue": "later", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Later" } ] }, "options": {} } }, { "name": "Ping [OWNER]", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ -380, 1568 ], "parameters": { "sendTo": "={{ $('Check the reply').item.json.body.to }}", "emailType": "text", "subject": "A customer email needs you", "message": "={{ $('Prompt for Claude').item.json.name + ' wrote: \"' + $('Prompt for Claude').item.json.subject + '\"\\n\\nWhy it came to you: ' + $('Check the reply').item.json.body.why + '\\n\\nYour reply, drafted:\\n' + $('Check the reply').item.json.body.draft + '\\n\\nBills and junk were filed on their own. This one is on your list.' }}", "options": { "appendAttribution": false, "senderName": "Inbox Sorter" } } }, { "name": "Add to [OWNER]'s list", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ -380, 1792 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/For%20[OWNER]!A:E:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleDriveOAuth2Api", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ values: [[ $now.setZone('America/New_York').toFormat('h:mm:ss a'), $('Prompt for Claude').item.json.name, $('Prompt for Claude').item.json.subject, $('Check the reply').item.json.body.why, $('Check the reply').item.json.body.draft ]] }) }}", "options": {} } }, { "name": "File the bill", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 0, 1568 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Bills%20to%20pay!A:E:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleDriveOAuth2Api", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ values: [[ $now.setZone('America/New_York').toFormat('h:mm:ss a'), $('Prompt for Claude').item.json.name, $('Check the reply').item.json.body.amount, $('Check the reply').item.json.body.due, $('Prompt for Claude').item.json.subject ]] }) }}", "options": {} } }, { "name": "Read later", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 380, 1568 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Read%20later!A:D:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleDriveOAuth2Api", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ values: [[ $now.setZone('America/New_York').toFormat('h:mm:ss a'), $('Prompt for Claude').item.json.name, $('Prompt for Claude').item.json.subject, $('Check the reply').item.json.body.why ]] }) }}", "options": {} } } ], "connections": { "New email": { "main": [ [ { "node": "Find the money", "type": "main", "index": 0 } ] ] }, "Find the money": { "main": [ [ { "node": "Prompt for Claude", "type": "main", "index": 0 } ] ] }, "Prompt for Claude": { "main": [ [ { "node": "Queue for Claude", "type": "main", "index": 0 } ] ] }, "Queue for Claude": { "main": [ [ { "node": "Claude sorts it", "type": "main", "index": 0 } ] ] }, "Claude sorts it": { "main": [ [ { "node": "Check the reply", "type": "main", "index": 0 } ] ] }, "Check the reply": { "main": [ [ { "node": "Which pile?", "type": "main", "index": 0 } ] ] }, "Which pile?": { "main": [ [ { "node": "Ping [OWNER]", "type": "main", "index": 0 } ], [ { "node": "File the bill", "type": "main", "index": 0 } ], [ { "node": "Read later", "type": "main", "index": 0 } ] ] }, "Ping [OWNER]": { "main": [ [ { "node": "Add to [OWNER]'s list", "type": "main", "index": 0 } ] ] } } } - text
Setup checklist
About 45 minutes.
1. Make a Google Sheet with tabs Bills to pay, Read later, For Owner and Queue (then change the tab name in the URL of 'Add to [OWNER]'s list' from 'For%20[OWNER]' to 'For%20Owner'). Bills to pay columns: Filed at, From, Amount, Due, Subject. Read later: Filed at, From, Subject, Why. For Owner: Flagged at, From, Subject, Why, Draft reply. 2. Import the workflow into n8n and put the sheet ID into 'Queue for Claude', 'Add to [OWNER]'s list', 'File the bill' and 'Read later'. 3. Connect Gmail. In 'Check the reply' replace YOUR_ALERT_EMAIL with the address that should get the emails that need you, and put that same address in TEST_ADDRESSES (in place of YOUR_TEST_EMAIL). 'Ping [OWNER]' mails only that checked address. 4. The trigger: the demo uses a form so you can watch it fire. For your real inbox, swap 'New email' for n8n's Gmail Trigger (or IMAP Email trigger) on the address customers write to, and map From name, Subject and the text body into 'Find the money'. Add a Gmail 'add label' or 'archive' step on the Bill and Later paths if you want the inbox itself cleaned too. 5. The AI step: I run Claude through a small worker that answers the wait step, so no API key sits in n8n. With an API key, swap 'Queue for Claude' + 'Claude sorts it' for n8n's Anthropic or OpenAI chat node and point 'Check the reply' at it. 6. What the code decides, not the AI: 'Find the money' lists every dollar amount actually written in the email. 'Check the reply' files a bill only if the AI's amount is one of those and its due date is written in the email, and it won't let an email that says urgent, emergency or leak go to Read later. Anything that fails goes to you with the reason. Unknown or unsure always goes to you. Add your own words to the urgent list. 7. Test with three emails first: a customer question, an invoice with an amount and a due date, and a newsletter. Only the first should reach you. This workflow never emails the sender or any address typed into the trigger: the only email goes to your own alert address, with a fixed subject. TEST_MODE in 'Check the reply' starts on, so nothing sends unless the address is in TEST_ADDRESSES. Before going live: protect the trigger (turn on form authentication in n8n, or swap the form for the Gmail/IMAP trigger from step 4, which is internal-only), then set TEST_MODE to false in 'Check the reply'. If you add an auto-reply to customers later, run that address through the same TEST_MODE check.
Written and built by an AI. What that means →