AutomationFlowsAI & RAG › Host an AI Gmail Reply Approval Inbox with Openai and Customjs

Host an AI Gmail Reply Approval Inbox with Openai and Customjs

ByCustomJS @customjs on n8n.io

This workflow demonstrates how to automatically generate and host a premium, interactive human-in-the-loop email approval dashboard using CustomJS and OpenAI.

Event trigger★★★★☆ complexityAI-powered16 nodes@Custom Js/N8N Nodes Pdf Toolkit V2GmailOpenAI
AI & RAG Trigger: Event Nodes: 16 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #15865 — we link there as the canonical source.

This workflow follows the Gmail → OpenAI recipe pattern — see all workflows that pair these two integrations.

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
{
  "id": "RGkUW7zWOZIU2Gqw",
  "name": "AI Email Reply Assistant",
  "tags": [],
  "nodes": [
    {
      "id": "e2048ffc-e1be-4585-bc37-d6b5a9d0f08e",
      "name": "When clicking 'Execute workflow'",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        448,
        352
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "1e2287ad-16dd-4425-87e9-24e9665d54d0",
      "name": "Upsert Approval Page",
      "type": "@custom-js/n8n-nodes-pdf-toolkit-v2.pdfToolkit",
      "position": [
        1824,
        352
      ],
      "parameters": {
        "pageName": "=Dashboard",
        "resource": "page",
        "operation": "upsert",
        "htmlContent": "={{ $json.html }}"
      },
      "credentials": {
        "customJsApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b389898a-9936-4eda-bef0-4a0d543c2f65",
      "name": "Sticky Note Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        16
      ],
      "parameters": {
        "color": 5,
        "width": 380,
        "height": 972,
        "content": "# AI Email Reply Assistant\n\n### \ud83d\ude80 Overview\nThis two-lane workflow automates email draft generation using AI, hosts an interactive human-in-the-loop approval dashboard, and dispatches approved replies.\n\n### \ud83d\udce5 Lane 1: Draft Generator\n1. **Fetch Emails:** Retrieves recent unread messages from your Gmail inbox.\n2. **Filter Out Spam:** Excludes automated/no-reply senders.\n3. **Generate AI Draft:** An LLM generates a personalized draft reply.\n4. **Build Dashboard:** Bundles pending emails into an inbox-style Tailwind dashboard hosted via **CustomJS**.\n\n### \ud83d\udce4 Lane 2: Response Handler\n1. **Receive Approval:** Webhook captures your edits and approval from the dashboard.\n2. **Send Reply:** Sends the reply as a threaded Gmail response.\n3. **Mark Read:** Archives the email by marking it as read.\n\n### \u2699\ufe0f Initial Setup\n1. **Gmail Credentials:** Connect your Gmail account in the *Get many messages*, *Reply to a message*, and *Mark a message as read* nodes.\n2. **OpenAI Credentials:** Connect your OpenAI API key in the *Generate AI Draft Reply* node.\n3. **CustomJS Credentials:** Connect your CustomJS credentials in the *Upsert Approval Page* node.\n4. **Activate:** Toggle the workflow to **Active** so the Lane 2 Webhook can receive submissions."
      },
      "typeVersion": 1
    },
    {
      "id": "040903ff-0fe2-4053-9689-dce9041985fe",
      "name": "Sticky Note Email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        16
      ],
      "parameters": {
        "color": 5,
        "width": 560,
        "height": 300,
        "content": "### \ud83d\udcec Email Source\nThis section retrieves unread emails to process.\n\n- **Trigger:** Currently manual execution. For production, replace the manual trigger with a **Gmail Trigger** (On Email Received) or an **IMAP Email** trigger.\n- **Gmail Node:** Fetches unread emails. Customize the fetch limit (currently 3) in the parameters as needed.\n- **Filter Node:** A JavaScript code block that automatically filters out standard `no-reply@` addresses to keep your dashboard clean."
      },
      "typeVersion": 1
    },
    {
      "id": "b4f80070-8748-4ffc-b901-2648b3e6af5a",
      "name": "Sticky Note AI",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        16
      ],
      "parameters": {
        "color": 5,
        "width": 364,
        "height": 300,
        "content": "### \ud83e\udd16 AI Reply Generation\nGenerates a contextual response draft.\n\n- **OpenAI Node:** Uses `gpt-5` (or your preferred LLM) to analyze the email body, sender, and subject.\n- **Prompt:** Instructs the model to write a professional email reply based on the sender's input.\n- **Customization:** Modify the prompt template or system instructions to match your desired tone, style, or business context."
      },
      "typeVersion": 1
    },
    {
      "id": "6a4e4d7e-79b3-480e-b979-ee5edfa6e01f",
      "name": "Sticky Note Host",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        16
      ],
      "parameters": {
        "color": 5,
        "width": 640,
        "height": 300,
        "content": "### \ud83c\udf10 Host Approval Dashboard\nCompiles the email inbox and hosts the user interface.\n\n- **Format Drafts:** Maps email fields and pairs them with their respective AI draft responses.\n- **Build Approval Page:** A Code node containing a premium Tailwind CSS + Alpine.js web app. It bundles all pending emails into a single-page app where you can view side-by-side comparisons and edit the draft.\n- **Upsert Approval Page (CustomJS):** Automatically hosts the generated HTML on the CustomJS CDN."
      },
      "typeVersion": 1
    },
    {
      "id": "6d08a274-6a10-42ae-a345-2927f7a6f328",
      "name": "Get many messages",
      "type": "n8n-nodes-base.gmail",
      "position": [
        640,
        352
      ],
      "parameters": {
        "limit": 10,
        "filters": {
          "readStatus": "unread"
        },
        "operation": "getAll"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e870c110-1271-4efd-b17c-13e537e99203",
      "name": "Webhook - Receive Approval",
      "type": "n8n-nodes-base.webhook",
      "position": [
        464,
        816
      ],
      "parameters": {
        "path": "email-reply-submit",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "lastNode"
      },
      "typeVersion": 1
    },
    {
      "id": "c57dd724-8b61-47db-b691-4b24713e4c47",
      "name": "Extract Reply Data",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        816
      ],
      "parameters": {
        "jsCode": "var body = $input.all()[0].json.body;\nvar replyData = {\n  approvedReply: body.approvedReply || \"\",\n  originalFrom: body.originalFrom || \"\",\n  originalFromName: body.originalFromName || \"\",\n  originalSubject: body.originalSubject || \"\",\n  messageId: body.messageId || \"\",\n  threadId: body.threadId || \"\",\n  approvedAt: new Date().toISOString()\n};\nreturn [{ json: replyData }];"
      },
      "typeVersion": 2
    },
    {
      "id": "b9b77f43-ee78-4c3d-851e-294bf2c21cba",
      "name": "Lane 2 Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        576
      ],
      "parameters": {
        "color": 5,
        "width": 1604,
        "height": 412,
        "content": "## Lane 2: Receive & Send Approved Replies\nThis lane handles the submission from the dashboard, sends the email, and updates the inbox status.\n\n1. **Webhook - Receive Approval:** Listens for the POST request containing the approved text and message ID.\n   *(Note: Set the workflow to Active for this webhook URL to work)*\n2. **Extract Reply Data:** Parses the incoming payload fields.\n3. **Reply to a message:** Sends the approved text as a threaded reply via Gmail.\n4. **Mark a message as read:** Marks the original message as read in Gmail, removing it from future draft generations."
      },
      "typeVersion": 1
    },
    {
      "id": "60c542e3-54cb-4807-8a3d-d5a4d9e66955",
      "name": "Filter No Reply Emails",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        352
      ],
      "parameters": {
        "jsCode": "const filtered = $input.all().filter(item => {\n  const json = item.json || {};\n\n  const from =\n    (json.from ||\n     json.From ||\n     json.sender ||\n     \"\").toLowerCase();\n\n  // Exclude common no-reply patterns\n  const blockedPatterns = [\n    \"no-reply\",\n    \"noreply\",\n    \"do-not-reply\",\n    \"donotreply\"\n  ];\n\n  return !blockedPatterns.some(pattern =>\n    from.includes(pattern)\n  );\n});\n\nreturn filtered;"
      },
      "typeVersion": 2
    },
    {
      "id": "05c18cad-9faf-4ddf-96f1-0643b9bdbebf",
      "name": "Generate AI Draft Reply",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1056,
        352
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5",
          "cachedResultName": "GPT-5"
        },
        "options": {},
        "responses": {
          "values": [
            {
              "content": "=Email Subject: {{ $json.Subject }},\nEmail Body: {{ $json.snippet }}\nEmail From: {{ $json.From }}\nEmail To: {{ $json.To }}\n\nCan you please just return the email reply no subject."
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "f65d636a-1837-45f1-a4b9-0d2cd8e34ff6",
      "name": "Format Email Drafts",
      "type": "n8n-nodes-base.code",
      "position": [
        1392,
        352
      ],
      "parameters": {
        "jsCode": "// Iterate on each input item.\n// This code is meant for an n8n Code node.\n\nconst items = $('Filter No Reply Emails').all();\nconst aiItems = $('Generate AI Draft Reply').all();\n\nreturn items.map((item, index) => {\n  const data = item.json;\n  const aiDraft = aiItems[index].json?.output?.[0]?.content?.[0]?.text;\n  \n  const email = {\n    from: data.From,\n    fromName: data.From,\n    subject: data.Subject,\n    body: data.snippet,\n    date: data.internalDate,\n    messageId: data.id,\n    threadId: data.threadId,\n    aiDraft: aiDraft,\n  };\n\n  return {\n    json: email,\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f92e4530-b581-465a-a735-09d837eb90c9",
      "name": "Build Approval Page",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        352
      ],
      "parameters": {
        "jsCode": "// Aggregates all email items into a single inbox-style HTML page.\n\nconst items = $input.all();\n\n// Normalize incoming emails safely\nconst emails = items.map(item => {\n  const json = item.json || {};\n\n  return {\n    from: json.from || \"\",\n    fromName: json.fromName || json.from || \"Unknown Sender\",\n    subject: json.subject || \"(No Subject)\",\n    body: json.body || \"\",\n    date: json.date || Date.now(),\n    messageId: json.messageId || Math.random().toString(36).slice(2),\n    threadId: json.threadId || \"\",\n    aiDraft: json.aiDraft || \"\",\n  };\n});\n\n// \u2699\ufe0f CONFIGURE: Set your n8n instance base URL here.\nconst N8N_BASE_URL = \"http://localhost:5678\";\n\n// Use \"webhook-test\" while testing, \"webhook\" in production.\nconst webhookPrefix = \"webhook-test\";\n\nconst SUBMIT_URL =\n  `${N8N_BASE_URL}/${webhookPrefix}/email-reply-submit`;\n\nconst safeEmails = JSON.stringify(emails)\n  .replace(/<\\/script>/gi, \"<\\\\/script>\");\n\nlet html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>AI Email Reply Assistant</title>\n\n  <script src=\"https://cdn.tailwindcss.com\"></script>\n  <script defer src=\"https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js\"></script>\n\n  <link href=\"https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n\n  <style>\n    body {\n      font-family: 'Plus Jakarta Sans', sans-serif;\n      background: #070b14;\n    }\n\n    [x-cloak] {\n      display: none !important;\n    }\n\n    .glass-card {\n      background: rgba(255,255,255,0.04);\n      backdrop-filter: blur(20px);\n      border: 1px solid rgba(255,255,255,0.08);\n    }\n\n    .email-item {\n      transition: all 0.2s ease;\n      cursor: pointer;\n      border-left: 3px solid transparent;\n    }\n\n    .email-item:hover {\n      background: rgba(255,255,255,0.05);\n    }\n\n    .email-item.active {\n      background: rgba(99,102,241,0.12);\n      border-left-color: #6366f1;\n    }\n\n    .reply-textarea {\n      background: rgba(255,255,255,0.06);\n      border: 1px solid rgba(255,255,255,0.12);\n      color: white;\n      resize: vertical;\n    }\n\n    .reply-textarea:focus {\n      outline: none;\n      border-color: #6366f1;\n    }\n\n    .send-btn {\n      background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);\n    }\n\n    .send-btn:hover {\n      opacity: 0.95;\n    }\n\n    .email-body-text {\n      white-space: pre-wrap;\n      word-wrap: break-word;\n    }\n\n    /* Slide-out animation for removed emails */\n    .email-remove {\n      animation: slideOut 0.25s ease forwards;\n    }\n\n    @keyframes slideOut {\n      to {\n        opacity: 0;\n        transform: translateX(-100%);\n        max-height: 0;\n        padding: 0;\n        margin: 0;\n        overflow: hidden;\n      }\n    }\n  </style>\n</head>\n\n<body class=\"min-h-screen text-white\">\n\n<div\n  x-data=\"emailApp()\"\n  x-cloak\n  class=\"min-h-screen flex flex-col\"\n>\n\n  <!-- Header -->\n  <div class=\"border-b border-white/10 px-6 py-4 flex justify-between items-center\">\n    <div>\n      <h1 class=\"text-xl font-bold\">\n        AI Email Reply Assistant\n      </h1>\n\n      <p class=\"text-slate-400 text-sm\">\n        Review and approve AI-generated replies\n      </p>\n    </div>\n\n    <div class=\"flex gap-3\">\n      <div class=\"px-3 py-1 rounded-full bg-yellow-500/20 text-yellow-300 text-sm\">\n        <span x-text=\"emails.length\"></span> Pending\n      </div>\n\n      <div class=\"px-3 py-1 rounded-full bg-green-500/20 text-green-300 text-sm\">\n        <span x-text=\"sentCount\"></span> Sent\n      </div>\n    </div>\n  </div>\n\n  <div class=\"flex flex-1 overflow-hidden\">\n\n    <!-- Sidebar -->\n    <div class=\"w-80 border-r border-white/10 overflow-y-auto\">\n\n      <!-- Empty state -->\n      <div\n        x-show=\"emails.length === 0\"\n        class=\"p-8 text-center text-slate-500 text-sm\"\n      >\n        <p class=\"text-2xl mb-2\">\ud83c\udf89</p>\n        <p>All emails handled!</p>\n      </div>\n\n      <template x-for=\"(email, idx) in emails\" :key=\"email.messageId\">\n\n        <div\n          @click=\"selectEmail(idx)\"\n          class=\"email-item p-4 border-b border-white/5\"\n          :class=\"{ active: selectedIdx === idx }\"\n          :id=\"'email-' + email.messageId\"\n        >\n\n          <div class=\"flex justify-between items-start mb-1\">\n            <p\n              class=\"font-semibold text-sm truncate\"\n              x-text=\"email.fromName\"\n            ></p>\n          </div>\n\n          <p\n            class=\"text-slate-300 text-xs truncate mb-1\"\n            x-text=\"email.subject\"\n          ></p>\n\n          <p\n            class=\"text-slate-500 text-xs truncate\"\n            x-text=\"(email.body || '').substring(0, 70)\"\n          ></p>\n\n        </div>\n\n      </template>\n    </div>\n\n    <!-- Content -->\n    <div class=\"flex-1 overflow-y-auto p-6\">\n\n      <div\n        x-show=\"selectedIdx === null\"\n        class=\"h-full flex items-center justify-center text-slate-500\"\n      >\n        <div x-show=\"emails.length > 0\">Select an email</div>\n        <div x-show=\"emails.length === 0\">No more emails to review</div>\n      </div>\n\n      <div\n        x-show=\"selectedEmail\"\n        class=\"grid grid-cols-1 lg:grid-cols-2 gap-6\"\n      >\n\n        <!-- Original -->\n        <div class=\"glass-card rounded-2xl p-6\">\n\n          <h2 class=\"font-semibold text-lg mb-4\">\n            Original Email\n          </h2>\n\n          <div class=\"space-y-2 text-sm mb-6\">\n            <p>\n              <span class=\"text-slate-400\">From:</span>\n              <span x-text=\"selectedEmail?.fromName\"></span>\n            </p>\n\n            <p>\n              <span class=\"text-slate-400\">Subject:</span>\n              <span x-text=\"selectedEmail?.subject\"></span>\n            </p>\n          </div>\n\n          <div\n            class=\"email-body-text text-slate-300 text-sm\"\n            x-text=\"selectedEmail?.body\"\n          ></div>\n        </div>\n\n        <!-- Reply -->\n        <div class=\"glass-card rounded-2xl p-6 flex flex-col\">\n\n          <h2 class=\"font-semibold text-lg mb-4\">\n            AI Draft Reply\n          </h2>\n\n          <textarea\n            x-model=\"replyText\"\n            rows=\"12\"\n            class=\"reply-textarea w-full rounded-xl p-4 text-sm flex-1\"\n          ></textarea>\n\n          <button\n            @click=\"sendReply\"\n            :disabled=\"sending\"\n            class=\"send-btn mt-4 py-3 rounded-xl font-semibold\"\n          >\n            <span x-show=\"!sending\">\n              Approve & Send\n            </span>\n\n            <span x-show=\"sending\">\n              Sending...\n            </span>\n          </button>\n\n          <div\n            x-show=\"error\"\n            class=\"mt-3 text-red-400 text-sm\"\n            x-text=\"error\"\n          ></div>\n\n        </div>\n\n      </div>\n    </div>\n  </div>\n</div>\n\n<script>\nfunction emailApp() {\n\n  const EMAILS = %%EMAILS_DATA%%;\n  const SUBMIT_URL = %%SUBMIT_URL%%;\n\n  return {\n\n    emails: EMAILS.map(e => ({ ...e })),\n\n    selectedIdx: null,\n    replyText: '',\n    sending: false,\n    error: null,\n    sentCount: 0,\n\n    get selectedEmail() {\n      return this.selectedIdx !== null\n        ? this.emails[this.selectedIdx]\n        : null;\n    },\n\n    selectEmail(idx) {\n      this.selectedIdx = idx;\n      this.replyText = this.emails[idx].aiDraft || '';\n      this.error = null;\n    },\n\n    async sendReply() {\n\n      const email = this.emails[this.selectedIdx];\n      const currentIdx = this.selectedIdx;\n\n      this.sending = true;\n      this.error = null;\n\n      try {\n\n        const res = await fetch(SUBMIT_URL, {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json'\n          },\n          body: JSON.stringify({\n            approvedReply: this.replyText,\n            originalFrom: email.from,\n            originalSubject: email.subject,\n            messageId: email.messageId,\n            threadId: email.threadId\n          })\n        });\n\n        if (!res.ok) {\n          throw new Error('Failed');\n        }\n\n        // \u2500\u2500 Animate out then remove from array \u2500\u2500\n        const el = document.getElementById('email-' + email.messageId);\n        if (el) {\n          el.classList.add('email-remove');\n          // Wait for animation to finish before splicing\n          await new Promise(resolve => setTimeout(resolve, 260));\n        }\n\n        this.sentCount++;\n        this.emails.splice(currentIdx, 1);\n\n        // Auto-select next email, or previous if we were at the end\n        if (this.emails.length === 0) {\n          this.selectedIdx = null;\n          this.replyText = '';\n        } else {\n          const nextIdx = currentIdx < this.emails.length\n            ? currentIdx\n            : currentIdx - 1;\n          this.selectEmail(nextIdx);\n        }\n\n      } catch (e) {\n\n        this.error = 'Failed to send reply';\n\n      } finally {\n\n        this.sending = false;\n      }\n    }\n  };\n}\n</script>\n\n</body>\n</html>`;\n\nhtml = html.replace(\n  \"%%EMAILS_DATA%%\",\n  safeEmails\n);\n\nhtml = html.replace(\n  \"%%SUBMIT_URL%%\",\n  JSON.stringify(SUBMIT_URL)\n);\n\nreturn [\n  {\n    json: {\n      html\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "843abcb3-3ce4-4c56-bdb6-d429b5e03fe6",
      "name": "Reply to a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1184,
        816
      ],
      "parameters": {
        "message": "={{ $json.approvedReply }}",
        "options": {},
        "emailType": "text",
        "messageId": "={{ $json.messageId }}",
        "operation": "reply"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "902e6dab-22d3-4351-b944-23dfe4d9bc65",
      "name": "Mark a message as read",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1584,
        816
      ],
      "parameters": {
        "messageId": "={{ $('Extract Reply Data').item.json.messageId }}",
        "operation": "markAsRead"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "dfabf0cb-2e1e-4250-928c-09d83895ae5f",
  "connections": {
    "Get many messages": {
      "main": [
        [
          {
            "node": "Filter No Reply Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Reply Data": {
      "main": [
        [
          {
            "node": "Reply to a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reply to a message": {
      "main": [
        [
          {
            "node": "Mark a message as read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Approval Page": {
      "main": [
        [
          {
            "node": "Upsert Approval Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Email Drafts": {
      "main": [
        [
          {
            "node": "Build Approval Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter No Reply Emails": {
      "main": [
        [
          {
            "node": "Generate AI Draft Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate AI Draft Reply": {
      "main": [
        [
          {
            "node": "Format Email Drafts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Receive Approval": {
      "main": [
        [
          {
            "node": "Extract Reply Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Get many messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

This workflow demonstrates how to automatically generate and host a premium, interactive human-in-the-loop email approval dashboard using CustomJS and OpenAI.

Source: https://n8n.io/workflows/15865/ — 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

This n8n template demonstrates how to automate email classification, labeling, draft generation, and logging using Gmail, OpenAI, and Google Sheets. Use cases include customer support management, sale

Gmail Trigger, Gmail, Text Classifier +4
AI & RAG

This n8n template uses AI to automatically classify incoming Gmail messages into five categories and route them to the right people or departments. It can also reply automatically and send WhatsApp al

Gmail Trigger, OpenAI Chat, Gmail +3
AI & RAG

This workflow acts as your personal inbox assistant. It automatically filters, classifies, and responds to incoming emails using AI, saving you from manually sorting through leads or inquiries 24/7.

OpenAI, Gmail Trigger, Google Sheets +3
AI & RAG

This workflow is ideal for HR professionals, recruiters, and small businesses looking to streamline resume screening with AI-powered analysis and CRM integration.

Jot Form Trigger, Postgres, OpenAI +5
AI & RAG

Detects new unread Gmail messages Extracts sender name for personalized replies Classifies the email into one of four categories Applies the correct Gmail label and either sends an auto-reply, creates

Gmail Trigger, OpenAI Chat, Gmail +4