{
  "name": "M10 Triage with approval gate",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "subject:\"[S10 test]\" -subject:\"Re:\" -subject:\"Approve this reply\""
        },
        "options": {}
      },
      "id": "f1b4ed45-70e9-49aa-bcd3-c43571826aed",
      "name": "New email",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You classify incoming customer emails for a small software company.\nRead the email below and reply with exactly one word from this list:\nbilling, technical, sales, unsure\nRules:\n- billing: invoices, payments, refunds, subscription changes\n- technical: errors, login problems, how-to questions about the product\n- sales: pricing, demos, new licences, partnerships\n- unsure: anything else, or if two labels fit equally well\nReply with the single word only. No punctuation, no explanation.\nEmail subject: {{ $json.subject }}\nEmail body: {{ $json.text }}",
        "batching": {}
      },
      "id": "9b006f37-eec9-4ea6-bd20-02aa77271693",
      "name": "Classify email",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        240,
        0
      ]
    },
    {
      "parameters": {
        "modelName": "models/gemini-2.5-flash",
        "options": {
          "temperature": 0
        }
      },
      "id": "be9ff5fe-c42d-46d1-b304-f7d95c3ec6d8",
      "name": "Gemini (flash)",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "typeVersion": 1,
      "position": [
        520,
        240
      ],
      "credentials": {
        "googlePalmApi": {
          "id": null,
          "name": "",
          "__aiGatewayManaged": true
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e1b8eb03-36f6-4c32-b257-905f9fac4330",
              "name": "label",
              "value": "={{ $json.text.trim().toLowerCase() }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "32c61209-8a88-48b1-9c80-f59c01de32cc",
      "name": "Clean label",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        560,
        0
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You draft replies to customer emails for the support team of a small software company.\nWrite a short, polite reply (under 120 words) to the email below. Category: {{ $json.label }}.\nDo not promise refunds, discounts, dates or fixes; say what happens next and that a colleague will follow up if needed.\nSign off as \"Customer Support\". Reply with the email body only: no subject line, no commentary.\nEmail subject: {{ $('New email').item.json.subject }}\nEmail body: {{ $('New email').item.json.text }}",
        "batching": {}
      },
      "id": "40e6b552-50c6-4839-99ff-a449683b7cbe",
      "name": "Draft reply",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        800,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "edac9705-9d69-400a-ac91-9ee46aab45fa",
              "name": "message_id",
              "value": "={{ $('New email').item.json.id }}",
              "type": "string"
            },
            {
              "id": "e55c36a6-9c62-4cd0-a177-d0bfc1f3295b",
              "name": "to",
              "value": "={{ $('New email').item.json.from.text }}",
              "type": "string"
            },
            {
              "id": "7ea1a5c6-83b7-4b01-9881-d2e7476e8f1e",
              "name": "subject",
              "value": "=Re: {{ $('New email').item.json.subject }}",
              "type": "string"
            },
            {
              "id": "61523502-ee80-491c-b31d-8f18120c6109",
              "name": "label",
              "value": "={{ $('Clean label').item.json.label }}",
              "type": "string"
            },
            {
              "id": "30a7a3a3-ff8d-4682-925b-6e739c752789",
              "name": "reply",
              "value": "={{ $json.text.trim() }}",
              "type": "string"
            },
            {
              "id": "00d3084d-accf-46ae-bb34-8688269f56a5",
              "name": "approver",
              "value": "PASTE-THE-APPROVER-GMAIL-ADDRESS-HERE",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": false,
        "options": {}
      },
      "id": "0ecdf216-1a91-4214-9ca7-0e66c244a0cf",
      "name": "Keep reply",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1120,
        0
      ]
    },
    {
      "parameters": {
        "operation": "sendAndWait",
        "sendTo": "={{ $json.approver }}",
        "subject": "=Approve this reply? {{ $json.subject }}",
        "message": "=The triage workflow wants to send this reply. Nothing is sent until you approve.\n\nTo: {{ $json.to }}\nSubject: {{ $json.subject }}\nCategory: {{ $json.label }}\n\n--- reply ---\n{{ $json.reply }}\n--- end ---\n\nNo answer within 24 hours: nothing is sent and the e-mail is labelled needs-human.",
        "responseType": "approval",
        "approvalOptions": {
          "values": {
            "approvalType": "double",
            "approveLabel": "Approve and send",
            "disapproveLabel": "Reject"
          }
        },
        "options": {
          "limitWaitTime": {
            "values": {
              "limitType": "afterTimeInterval",
              "resumeAmount": 24,
              "resumeUnit": "hours"
            }
          },
          "appendAttribution": false
        }
      },
      "id": "5a4bbde3-2aa4-42c0-98ea-0b9d89a0a99e",
      "name": "Ask for approval",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1360,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "b2bd49f7-977e-4dfe-9b6b-baab53395a4f",
              "name": "decision",
              "value": "={{ $json.data?.approved === true ? 'approved' : ($json.data?.approved === false ? 'rejected' : 'timed out') }}",
              "type": "string"
            },
            {
              "id": "8c928cb0-5f3c-42c0-ba99-73247ae2cdb1",
              "name": "message_id",
              "value": "={{ $('Keep reply').item.json.message_id }}",
              "type": "string"
            },
            {
              "id": "41495455-4eec-4a10-81ff-d815fad4e3ad",
              "name": "to",
              "value": "={{ $('Keep reply').item.json.to }}",
              "type": "string"
            },
            {
              "id": "8cdee4eb-1989-4f44-adea-4659e1f202f3",
              "name": "subject",
              "value": "={{ $('Keep reply').item.json.subject }}",
              "type": "string"
            },
            {
              "id": "35a33ca2-b564-4029-b2dd-e7ee4dc9952e",
              "name": "reply",
              "value": "={{ $('Keep reply').item.json.reply }}",
              "type": "string"
            },
            {
              "id": "685c9233-c0b3-442d-b4f2-0ec8459e9b8f",
              "name": "approver",
              "value": "={{ $json.data?.approved === undefined ? 'nobody (timed out)' : $('Keep reply').item.json.approver }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": false,
        "options": {}
      },
      "id": "b5417b47-1f55-4fb9-a2ae-fad2c090801a",
      "name": "Decision",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1600,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "345966d5-05a3-4a7e-8a81-b249bed0d752",
              "leftValue": "={{ $json.decision }}",
              "rightValue": "approved",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "54eb99f2-7d36-49c8-9421-eb3fa4209440",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1840,
        0
      ]
    },
    {
      "parameters": {
        "operation": "reply",
        "messageId": "={{ $json.message_id }}",
        "emailType": "text",
        "message": "={{ $json.reply }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "3694ae51-c2a7-4f48-8c79-7d0b43d77004",
      "name": "Send reply",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2080,
        -120
      ]
    },
    {
      "parameters": {
        "operation": "addLabels",
        "messageId": "={{ $json.message_id }}",
        "labelIds": [
          "PICK-LABEL-needs-human"
        ]
      },
      "id": "b440f53c-4fee-41f7-9c31-4b3e3487ba89",
      "name": "Label needs human",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2080,
        140
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "PICK-THE-APPROVAL-LOG-SHEET"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Approval log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "value": {
            "timestamp": "={{ $now.toFormat('yyyy-MM-dd HH:mm:ss') }}",
            "workflow": "={{ $workflow.name }}",
            "run_id": "={{ $execution.id }}",
            "action_proposed": "=Reply to {{ $('Decision').item.json.to }}: {{ $('Decision').item.json.subject }}",
            "tier": "4",
            "approver": "={{ $('Decision').item.json.approver }}",
            "decision": "={{ $('Decision').item.json.decision }}",
            "final_content": "={{ $('Decision').item.json.reply }}",
            "notes": "sent as approved"
          },
          "schema": [
            {
              "id": "timestamp",
              "displayName": "timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "workflow",
              "displayName": "workflow",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "run_id",
              "displayName": "run_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "action_proposed",
              "displayName": "action_proposed",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "tier",
              "displayName": "tier",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "approver",
              "displayName": "approver",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "decision",
              "displayName": "decision",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "final_content",
              "displayName": "final_content",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "notes",
              "displayName": "notes",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "ce668694-9fb5-4429-9101-25d812382e8e",
      "name": "Log sent",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2320,
        -120
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "PICK-THE-APPROVAL-LOG-SHEET"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Approval log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "value": {
            "timestamp": "={{ $now.toFormat('yyyy-MM-dd HH:mm:ss') }}",
            "workflow": "={{ $workflow.name }}",
            "run_id": "={{ $execution.id }}",
            "action_proposed": "=Reply to {{ $('Decision').item.json.to }}: {{ $('Decision').item.json.subject }}",
            "tier": "4",
            "approver": "={{ $('Decision').item.json.approver }}",
            "decision": "={{ $('Decision').item.json.decision }}",
            "final_content": "",
            "notes": "=not sent; labelled needs-human ({{ $('Decision').item.json.decision }})"
          },
          "schema": [
            {
              "id": "timestamp",
              "displayName": "timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "workflow",
              "displayName": "workflow",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "run_id",
              "displayName": "run_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "action_proposed",
              "displayName": "action_proposed",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "tier",
              "displayName": "tier",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "approver",
              "displayName": "approver",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "decision",
              "displayName": "decision",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "final_content",
              "displayName": "final_content",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "notes",
              "displayName": "notes",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "0035d201-9c28-4437-abbb-e77a99e9b253",
      "name": "Log not sent",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2320,
        140
      ]
    },
    {
      "parameters": {
        "content": "## M10 Triage with approval gate\nTier 1: trigger, **Classify**, **Draft reply** (nothing leaves yet). Tier 2: the label. Tier 4: **Send reply**, so it sits behind **Ask for approval** (Gmail, Send and wait for response, Approval).\n\nApproved: the reply is sent and logged. Rejected or no answer in time: nothing is sent, the e-mail is labelled needs-human and the decision is logged. Every gated action gets one row in the **Approval log**.",
        "width": 760,
        "height": 250,
        "color": 5
      },
      "id": "d1295d09-1726-4701-bf3f-e3efef7252c9",
      "name": "Note 8584",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -440
      ]
    },
    {
      "parameters": {
        "content": "## Three gate rules\n1. Show the actual action: To, Subject and the full reply are in the approval e-mail.\n2. Time out safely: no answer = not sent.\n3. Carry the decision forward: **Decision** names it, both branches log it.",
        "width": 560,
        "height": 200,
        "color": 4
      },
      "id": "0bb65314-7ab7-4238-a5f0-747ddc888cb2",
      "name": "Note c310",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1340,
        -400
      ]
    }
  ],
  "connections": {
    "New email": {
      "main": [
        [
          {
            "node": "Classify email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini (flash)": {
      "ai_languageModel": [
        [
          {
            "node": "Classify email",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Draft reply",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Classify email": {
      "main": [
        [
          {
            "node": "Clean label",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean label": {
      "main": [
        [
          {
            "node": "Draft reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft reply": {
      "main": [
        [
          {
            "node": "Keep reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep reply": {
      "main": [
        [
          {
            "node": "Ask for approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask for approval": {
      "main": [
        [
          {
            "node": "Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decision": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved?": {
      "main": [
        [
          {
            "node": "Send reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Label needs human",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send reply": {
      "main": [
        [
          {
            "node": "Log sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Label needs human": {
      "main": [
        [
          {
            "node": "Log not sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Amman"
  },
  "meta": {}
}