{
  "name": "Email \u2192 AI triage \u2192 CRM pipeline (template)",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyX",
              "value": 1,
              "unit": "minutes"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "has:attachment filename:pdf"
        },
        "options": {
          "dataPropertyAttachmentsPrefixName": "attachment_",
          "downloadAttachments": true
        }
      },
      "id": "1f0a6297-192e-4c86-a3d9-044f1476b891",
      "name": "Email Received",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.3,
      "position": [
        -800,
        0
      ]
    },
    {
      "parameters": {
        "operation": "binaryToPropery",
        "binaryPropertyName": "attachment_0",
        "options": {}
      },
      "id": "130f68ae-0b7b-47e6-ab4f-9c1d41ae2e49",
      "name": "Extract PDF Text",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        -576,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const from = $('Email Received').item.json.from?.text || '';\nconst subject = $('Email Received').item.json.subject || '';\nconst body = ($('Email Received').item.json.text || '').substring(0, 800);\n\n// CUSTOMIZE: replace these categories and rules with your own.\nconst systemPrompt = `You are an email triage assistant. Classify the email into EXACTLY one category. Reply with a single word only: QUOTE, FOLLOW_UP, OTHER.\n\n- QUOTE: a new inbound request that needs data extracted from the attachment and a quote prepared.\n- FOLLOW_UP: a customer replying about an existing request (a question, an acceptance, a change).\n- OTHER: anything else (newsletters, internal mail, spam).`;\n\nreturn {\n  json: {\n    body: JSON.stringify({\n      model: 'claude-haiku-4-5',\n      max_tokens: 16,\n      temperature: 0,\n      system: systemPrompt,\n      messages: [{ role: 'user', content: 'From: ' + from + '\\nSubject: ' + subject + '\\nBody: ' + body }]\n    })\n  }\n};"
      },
      "id": "a9a66c33-0c13-427a-8fb9-15dcf456ef50",
      "name": "Build Triage Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -368,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ $json.body }}",
        "options": {}
      },
      "id": "d4e4ce2d-d343-48cf-b0f6-165c5c9f6dbf",
      "name": "Claude Triage (Haiku)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -144,
        0
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.content[0].text.trim() }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "QUOTE"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "QUOTE"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.content[0].text.trim() }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "FOLLOW_UP"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "FOLLOW_UP"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "OTHER"
        }
      },
      "id": "72ba7122-4a99-42bf-80bb-05602817c268",
      "name": "Route by Category",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        80,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const pdfBase64 = $('Extract PDF Text').item.json.data;\nconst subject = $('Email Received').item.json.subject || '';\nconst body = ($('Email Received').item.json.text || '').substring(0, 2000);\n\n// CUSTOMIZE: describe the fields you want and any business rules.\n// Keep calculations OUT of the model \u2014 let the AI read, let your sheet/code compute.\nconst systemPrompt = `You extract structured data from the attached PDF and return JSON only.\n\nReturn ONLY raw JSON, no preamble, no markdown fences.\nSchema: { \"customer_name\": string, \"reference\": string, \"destination\": string, \"amount\": number }`;\n\nreturn {\n  json: {\n    body: {\n      model: 'claude-sonnet-4-5',\n      max_tokens: 1024,\n      temperature: 0,\n      system: systemPrompt,\n      messages: [{\n        role: 'user',\n        content: [\n          { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: pdfBase64 } },\n          { type: 'text', text: 'Subject: ' + subject + '\\n\\n' + body }\n        ]\n      }]\n    }\n  }\n};"
      },
      "id": "b2d9fbe7-e3ce-4c20-80b8-973aafb5a94b",
      "name": "Build Extraction Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        304,
        -208
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            },
            {
              "name": "anthropic-beta",
              "value": "pdfs-2024-09-25"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json.body) }}",
        "options": {}
      },
      "id": "9839f088-f6b6-4e98-90a5-a8c275e4a7f3",
      "name": "Claude Extract (Sonnet)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        528,
        -208
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const content = $input.item.json.content || [];\nconst textBlock = content.find(b => b.type === 'text');\nconst raw = textBlock ? textBlock.text : '{}';\nconst clean = raw.replace(/```json/g, '').replace(/```/g, '').trim();\nlet data;\ntry {\n  data = JSON.parse(clean);\n} catch (e) {\n  throw new Error('Could not parse JSON from model output: ' + clean.slice(0, 200));\n}\n\n// Carry a couple of email metadata fields for the CRM / draft\nconst gmail = $('Email Received').item.json;\ndata.email_client = (gmail.from && gmail.from.text) || '';\ndata.thread_id = gmail.threadId || '';\nreturn { json: data };"
      },
      "id": "32eae7d1-d5fd-4be4-96df-3a8cb94a87d4",
      "name": "Parse JSON",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        752,
        -208
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "id": "fb92750b-c969-4cc0-b56d-b638bb6ed986",
      "name": "Append to CRM",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        960,
        -208
      ]
    },
    {
      "parameters": {
        "resource": "draft",
        "subject": "=Quote \u2014 {{ $('Parse JSON').item.json.customer_name }} \u2014 {{ $('Parse JSON').item.json.reference }}",
        "message": "=Hello {{ $('Parse JSON').item.json.customer_name }},\n\n[CUSTOMIZE: your reply template here]\n\nReference: {{ $('Parse JSON').item.json.reference }}",
        "options": {
          "threadId": "={{ $('Parse JSON').item.json.thread_id }}",
          "sendTo": "={{ $('Parse JSON').item.json.email_client }}"
        }
      },
      "id": "4ec7867b-767d-460f-8010-feb9ceeaf5fe",
      "name": "Draft Reply",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        1184,
        -208
      ]
    },
    {
      "parameters": {
        "sendTo": "<__PLACEHOLDER_VALUE__Your team inbox, e.g. team@yourcompany.com__>",
        "subject": "=Follow-up from {{ $('Email Received').item.json.from?.text }}",
        "message": "=A customer replied about an existing request.\n\nFrom: {{ $('Email Received').item.json.from?.text }}\nSubject: {{ $('Email Received').item.json.subject }}\n\n{{ ($('Email Received').item.json.text || '').substring(0, 800) }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "0a274454-be09-4b1b-9f02-de2f88e9c738",
      "name": "Notify: Follow-up",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        304,
        112
      ]
    },
    {
      "parameters": {
        "sendTo": "<__PLACEHOLDER_VALUE__Your team inbox, e.g. team@yourcompany.com__>",
        "subject": "=Unclassified email \u2014 {{ $('Email Received').item.json.from?.text }}",
        "message": "=This email was not classified as QUOTE or FOLLOW_UP.\n\nFrom: {{ $('Email Received').item.json.from?.text }}\nSubject: {{ $('Email Received').item.json.subject }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "5e508dbc-5353-4327-8b21-cef8fedbcbf9",
      "name": "Notify: Unclassified",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        304,
        368
      ]
    },
    {
      "parameters": {},
      "id": "c3a40ceb-2dc0-4b18-8b9f-d391522565e1",
      "name": "On Error",
      "type": "n8n-nodes-base.errorTrigger",
      "typeVersion": 1,
      "position": [
        -800,
        -400
      ]
    },
    {
      "parameters": {
        "sendTo": "<__PLACEHOLDER_VALUE__Your team inbox, e.g. team@yourcompany.com__>",
        "subject": "=Workflow error: {{ $json.execution.error.node.name }}",
        "message": "=Node: {{ $json.execution.error.node.name }}\nError: {{ $json.execution.error.message }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "dc770160-5547-4715-a6ff-e627d23a217e",
      "name": "Send Error Alert",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -576,
        -400
      ]
    },
    {
      "parameters": {
        "content": "## 1) Binary rule\nExtract From File MUST sit right after the trigger \u2014 PDF binary data does not survive Code/HTTP nodes.",
        "height": 200,
        "width": 300,
        "color": 4
      },
      "id": "1970e7a7-7731-4ad6-9784-18e5fe776ef7",
      "name": "Sticky Note d3cfaab3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -624,
        -48
      ]
    },
    {
      "parameters": {
        "content": "## 2) Customize your prompts\nEdit the two Code nodes: triage categories (Haiku) and extraction schema (Sonnet). Keep price/maths out of the model \u2014 AI reads, your sheet/code calculates.",
        "height": 400,
        "width": 960,
        "color": 3
      },
      "id": "af006176-91d2-4286-859a-f697d8b7c243",
      "name": "Sticky Note 5bd13e62",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -416,
        -256
      ]
    },
    {
      "parameters": {
        "content": "## 3) Set credentials & targets\nAdd your Gmail + Anthropic API + Google Sheets credentials, pick your sheet, and set the team inbox on the notify nodes.",
        "height": 200,
        "width": 300,
        "color": 5
      },
      "id": "d5b1273a-44e0-47f3-99ef-9befacf084d7",
      "name": "Sticky Note 68727839",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        912,
        -256
      ]
    }
  ],
  "connections": {
    "Email Received": {
      "main": [
        [
          {
            "node": "Extract PDF Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract PDF Text": {
      "main": [
        [
          {
            "node": "Build Triage Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Triage Prompt": {
      "main": [
        [
          {
            "node": "Claude Triage (Haiku)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Triage (Haiku)": {
      "main": [
        [
          {
            "node": "Route by Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Category": {
      "main": [
        [
          {
            "node": "Build Extraction Prompt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Notify: Follow-up",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Notify: Unclassified",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Extraction Prompt": {
      "main": [
        [
          {
            "node": "Claude Extract (Sonnet)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Extract (Sonnet)": {
      "main": [
        [
          {
            "node": "Parse JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse JSON": {
      "main": [
        [
          {
            "node": "Append to CRM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to CRM": {
      "main": [
        [
          {
            "node": "Draft Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On Error": {
      "main": [
        [
          {
            "node": "Send Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": true
  },
  "versionId": "aaa7fa4c-1f05-4107-b06c-cc232bea8ac5",
  "meta": {
    "aiBuilderAssisted": true,
    "builderVariant": "mcp"
  },
  "nodeGroups": [],
  "id": "uFEwYUKjb1OzSNPM",
  "tags": []
}