AutomationFlowsAI & RAG › Inbox Agent — Cli Rehearsal (ep11)

Inbox Agent — Cli Rehearsal (ep11)

Inbox Agent — CLI rehearsal (ep11). Event-driven trigger; 8 nodes.

Event trigger★★★★☆ complexity8 nodes
AI & RAG Trigger: Event Nodes: 8 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "name": "Inbox Agent \u2014 CLI rehearsal (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"
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Inbox Agent — CLI rehearsal (ep11). Event-driven trigger; 8 nodes.

Source: https://github.com/Ships-Itself/builds/blob/main/ep11-inbox-agent/workflow-rehearsal.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

agente. Uses googleTasksTool, telegramTrigger, telegramTool, telegram. Event-driven trigger; 94 nodes.

Google Tasks Tool, Telegram Trigger, Telegram Tool +3
AI & RAG

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

Data Table, HTTP Request
AI & RAG

Inquiry-Agent. Uses @digitalocean/n8n-nodes-digitalocean-gradient-serverless-inference, stopAndError, googleDocs, gmail. Event-driven trigger; 59 nodes.

@Digitalocean/N8N Nodes Digitalocean Gradient Serverless Inference, Stop And Error, Google Docs +4
AI & RAG

AI Social Media Automation for Multiple Platforms using Blotato. Uses telegramTrigger, @blotato/n8n-nodes-blotato, telegram, httpRequest. Event-driven trigger; 52 nodes.

Telegram Trigger, @Blotato/N8N Nodes Blotato, Telegram +1
AI & RAG

CLEAN Agent - Manual Trigger. Uses googleDrive, googleSheets, httpRequest. Event-driven trigger; 49 nodes.

Google Drive, Google Sheets, HTTP Request