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": "Inbox Agent \u2014 mail/ replay (ep11)",
"nodes": [
{
"parameters": {},
"id": "e1100000-0000-4000-8000-000000000008",
"name": "When clicking Execute",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
200,
300
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const fs=require('fs'),B=$env.EP11_DIR,dir=`${B}/mail`;\nconst files=fs.readdirSync(dir).filter((f)=>f.endsWith('.json')).sort();\nif(!files.length) throw new Error('mail/ is empty \u2014 replay needs a real fetch first (B10)');\nreturn files.map((f)=>({json:JSON.parse(fs.readFileSync(`${dir}/${f}`,'utf8'))}));"
},
"id": "e1100000-0000-4000-8000-000000000001",
"name": "read-mail-dir",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
420,
300
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const fs=require('fs'),B=$env.EP11_DIR,d=$json;\nconst LED=JSON.parse(fs.readFileSync(`${B}/replied.json`,'utf8'));\nconst NEVER=/^(no-?reply|donotreply|do-not-reply|bounce|mailer-daemon|postmaster)/i;\nconst local=d.from.split('@')[0];\nlet stop=null;\nif(d.listUnsub||d.autoSub.toLowerCase()!=='no') stop='NOTICE_BULK';\nelse if(NEVER.test(local)) stop='NOTICE_NOREPLY';\nelse if(LED[d.messageId]) stop='DONE_ALREADY_REPLIED';\nreturn {json:{...d,stop,category:stop?'NOTICE':null,confidence:0}};"
},
"id": "e1100000-0000-4000-8000-000000000002",
"name": "prefilter",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
300
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const fs=require('fs'),https=require('https'),B=$env.EP11_DIR,d=$json;\nif(d.stop) return {json:d};\nconst CATS='BOOKING,SUPPORT,PRICING,COLD_PITCH,BILLING,PERSONAL';\nconst prompt=`Classify this email. Reply with JSON only: {\"category\":one of [${CATS}],`\n +`\"confidence\":0..1,\"reason\":\"one line\"}\\n\\nFrom: ${d.from}\\nSubject: ${d.subject}\\n\\n${d.text}`;\nconst res=await new Promise((ok,er)=>{const r=https.request('https://fal.run/fal-ai/any-llm',{method:'POST',headers:{Authorization:'Key '+$env.FAL_KEY,'Content-Type':'application/json'}},s=>{let x='';s.on('data',c=>x+=c);s.on('end',()=>ok({h:s.headers,d:x}))});r.on('error',er);r.end(JSON.stringify({model:'meta-llama/llama-4-scout',prompt}))});\nfs.writeFileSync(`${B}/raw/${d.uid}-classify.json`,res.d);\nfs.appendFileSync(`${B}/receipts.jsonl`,JSON.stringify({uid:d.uid,call:'classify',units:res.h['x-fal-billable-units']||null,at:new Date().toISOString()})+'\\n');\nlet o={},j={};try{o=JSON.parse(res.d)}catch(e){}\ntry{j=JSON.parse(String(o.output||'').match(/\\{[\\s\\S]*\\}/)?.[0]||'{}')}catch(e){}\nreturn {json:{...d,category:j.category||'PERSONAL',confidence:Number(j.confidence)||0,reason:String(j.reason||'').slice(0,160)}};"
},
"id": "e1100000-0000-4000-8000-000000000003",
"name": "classify-call",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
860,
300
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const fs=require('fs'),https=require('https'),B=$env.EP11_DIR,d=$json;\nif(d.stop||!['BOOKING','SUPPORT'].includes(d.category)) return {json:d};\nconst KB=fs.readFileSync(`${B}/knowledge.md`,'utf8');\nconst prompt=`You reply to email using ONLY the facts below. If the answer is not `\n +`there, say you will follow up \u2014 invent nothing.\\n\\n=== FACTS ===\\n${KB}\\n=== END ===\\n\\n`\n +`Email from ${d.from}:\\nSubject: ${d.subject}\\n${d.text}\\n\\n`\n +`Reply with JSON only: {\"reply\":\"...\",\"facts\":[\"exact sentence copied from FACTS\", ...]}`;\nconst res=await new Promise((ok,er)=>{const r=https.request('https://fal.run/fal-ai/any-llm',{method:'POST',headers:{Authorization:'Key '+$env.FAL_KEY,'Content-Type':'application/json'}},s=>{let x='';s.on('data',c=>x+=c);s.on('end',()=>ok({h:s.headers,d:x}))});r.on('error',er);r.end(JSON.stringify({model:'meta-llama/llama-4-scout',prompt}))});\nfs.writeFileSync(`${B}/raw/${d.uid}-draft.json`,res.d);\nfs.appendFileSync(`${B}/receipts.jsonl`,JSON.stringify({uid:d.uid,call:'draft',units:res.h['x-fal-billable-units']||null,at:new Date().toISOString()})+'\\n');\nlet o={},j={};try{o=JSON.parse(res.d)}catch(e){}\ntry{j=JSON.parse(String(o.output||'').match(/\\{[\\s\\S]*\\}/)?.[0]||'{}')}catch(e){}\nreturn {json:{...d,to:d.from,parsed:!!j.reply,draft:{reply:String(j.reply||''),facts:Array.isArray(j.facts)?j.facts.map(String):[]}}};"
},
"id": "e1100000-0000-4000-8000-000000000004",
"name": "draft-call",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1080,
300
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const fs=require('fs'),B=$env.EP11_DIR,d=$json,b=d.draft?.reply||'',F=d.draft?.facts||[];\nconst KB=fs.readFileSync(`${B}/knowledge.md`,'utf8'),LED=JSON.parse(fs.readFileSync(`${B}/replied.json`,'utf8'));\nconst QUOTE=KB.split('\\n').filter((l)=>!/^\\s*(#|INTERNAL:|TODO)/.test(l)).join('\\n');\nconst strip=(s)=>s.replace(/\\d{4}-\\d{2}-\\d{2}(T[\\d:.]+Z?)?/g,' ').replace(/\\bv?\\d+\\.\\d+(\\.\\d+)?\\b/g,' ').replace(/llama-[\\w.-]+/gi,' ');\nconst tok=(s)=>strip(s).match(/\\$?\\d[\\d,.]*%?|https?:\\/\\/[^\\s\"'<>)]+/g)||[];\nconst src=strip(F.join(' ')+' '+(d.text||''));\nconst RULES=[\n ['CATEGORY_NOT_SENDABLE',()=>['BOOKING','SUPPORT'].includes(d.category)],\n ['LOW_CONFIDENCE', ()=>d.confidence>=0.75],\n ['NO_FACTS_CITED', ()=>F.length>0],\n ['FACT_NOT_IN_KB', ()=>F.every((f)=>KB.includes(f.trim()))],\n ['FACT_NOT_QUOTABLE', ()=>F.every((f)=>QUOTE.includes(f.trim()))],\n ['UNSUPPORTED_TOKEN', ()=>tok(b).every((t)=>src.includes(t))],\n ['RECIPIENT_MISMATCH', ()=>d.to===d.from],\n ['ALREADY_REPLIED', ()=>!LED[d.messageId]]];\nconst rule=d.stop||(RULES.find(([,ok])=>!ok())||[null])[0],verdict=rule?'BLOCKED':'SEND';\nfs.appendFileSync(`${B}/decisions.jsonl`,JSON.stringify({uid:d.uid,messageId:d.messageId,from:d.from,category:d.category,confidence:d.confidence,parsed:!!d.parsed,verdict,rule,at:new Date().toISOString()})+'\\n');\nreturn {json:{...d,verdict,rule}};"
},
"id": "e1100000-0000-4000-8000-000000000005",
"name": "send-gate",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1300,
300
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const fs=require('fs'),B=$env.EP11_DIR,items=$input.all().map(i=>i.json);\nconst by=(k)=>items.reduce((a,x)=>(a[x[k]||'none']=(a[x[k]||'none']||0)+1,a),{});\nconst sent=items.filter(x=>x.verdict==='SEND');\nconst lines=[\n `messages: ${items.length}`,`sendable: ${sent.length}`,\n `blocked by rule: ${JSON.stringify(by('rule'))}`,`by category: ${JSON.stringify(by('category'))}`,\n '','THIS GATE CANNOT VERIFY:',\n '- whether the sender is who they say they are',\n '- whether a plausible-sounding request is a real one',\n '- anything the transport adds after this node runs'];\nfs.writeFileSync(`${B}/digest.txt`,lines.join('\\n'));\nfs.appendFileSync(`${B}/digests.log`,`=== ${new Date().toISOString()}\\n${lines.join('\\n')}\\n`);\nreturn $input.all();"
},
"id": "e1100000-0000-4000-8000-000000000006",
"name": "digest",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1520,
300
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const fs=require('fs'),B=$env.EP11_DIR,ME=$env.EP11_OPERATOR;\nif($env.EP11_MODE!=='live') return [];\nconst LED=JSON.parse(fs.readFileSync(`${B}/replied.json`,'utf8'));\nconst out=[{json:{to:ME,replyTo:ME,subject:`EP11 run \u2014 ${new Date().toISOString()}`,body:fs.readFileSync(`${B}/digest.txt`,'utf8')}}];\nfor(const {json:d} of $input.all()){\n if(d.verdict!=='SEND'||LED[d.messageId]) continue;\n LED[d.messageId]={to:d.to,at:new Date().toISOString()};\n fs.writeFileSync(`${B}/approved/${d.uid}.txt`,d.draft.reply);\n out.push({json:{to:d.to,replyTo:ME,subject:`Re: ${d.subject}`,body:d.draft.reply,uid:d.uid}});\n}\nfs.writeFileSync(`${B}/replied.json`,JSON.stringify(LED,null,1));\nreturn out;"
},
"id": "e1100000-0000-4000-8000-000000000007",
"name": "outbox",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1740,
300
]
}
],
"connections": {
"When clicking Execute": {
"main": [
[
{
"node": "read-mail-dir",
"type": "main",
"index": 0
}
]
]
},
"read-mail-dir": {
"main": [
[
{
"node": "prefilter",
"type": "main",
"index": 0
}
]
]
},
"prefilter": {
"main": [
[
{
"node": "classify-call",
"type": "main",
"index": 0
}
]
]
},
"classify-call": {
"main": [
[
{
"node": "draft-call",
"type": "main",
"index": 0
}
]
]
},
"draft-call": {
"main": [
[
{
"node": "send-gate",
"type": "main",
"index": 0
}
]
]
},
"send-gate": {
"main": [
[
{
"node": "digest",
"type": "main",
"index": 0
}
]
]
},
"digest": {
"main": [
[
{
"node": "outbox",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Inbox Agent — mail/ replay (ep11). Event-driven trigger; 8 nodes.
Source: https://github.com/Ships-Itself/builds/blob/main/ep11-inbox-agent/workflow-batch.json — 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.
agente. Uses googleTasksTool, telegramTrigger, telegramTool, telegram. Event-driven trigger; 94 nodes.
This is for SaaS founders, agency owners, and Sales Ops managers who use HubSpot but are tired of "toe-stepping." If your BDRs are accidentally emailing your AE’s active deals, or Marketing is blastin
Inquiry-Agent. Uses @digitalocean/n8n-nodes-digitalocean-gradient-serverless-inference, stopAndError, googleDocs, gmail. Event-driven trigger; 59 nodes.
AI Social Media Automation for Multiple Platforms using Blotato. Uses telegramTrigger, @blotato/n8n-nodes-blotato, telegram, httpRequest. Event-driven trigger; 52 nodes.
CLEAN Agent - Manual Trigger. Uses googleDrive, googleSheets, httpRequest. Event-driven trigger; 49 nodes.