{
  "name": "Project 3.1",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "readStatus": "unread"
        },
        "options": {
          "downloadAttachments": true
        }
      },
      "id": "trigger_gmail",
      "name": "Gmail Trigger",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.4,
      "position": [
        -976,
        304
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Subject: {{ $json.subject }}\nFrom: {{ $json.fromName }} <{{ $json.from }}>\nDate: {{ $json.date }}\n\nBody:\n{{ $json.body }}",
        "messages": {
          "messageValues": [
            {
              "message": "You are an intelligent email assistant – a smart secretary. Analyze the provided email and return ONLY a valid JSON object (no markdown, no code blocks, no extra text) with these exact fields:\n- email_type: string – one of: \"question\", \"information\", \"meeting\", \"advertisement\", \"work\", \"personal\", \"other\"\n- priority: string – one of: \"high\", \"medium\", \"low\"\n- needs_reply: boolean – true if the email requires a response (questions, meeting invites, direct requests); false for newsletters, ads, automated notifications\n- is_meeting: boolean – true if the email is about a meeting, appointment, event or calendar invitation\n- reply_text: string – if needs_reply is true, write a professional reply in the same language as the original email; otherwise empty string \"\"\n- meeting_date: string – if is_meeting is true, date in YYYY-MM-DD format; otherwise \"\"\n- meeting_time: string – if is_meeting is true, start time HH:MM (24h); use \"10:00\" if not specified; otherwise \"\"\n- meeting_topic: string – if is_meeting is true, the meeting title/topic; otherwise \"\"\n- reason: string – one sentence explaining your classification (max 120 chars)"
            }
          ]
        },
        "batching": {}
      },
      "id": "chain_analyze",
      "name": "Analyze Email",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        -560,
        336
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "model_openrouter",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        -560,
        512
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "jsCode": "const item = $input.first();\nconst email = $('Normalize Email').first().json;\n\n// Get text output from chainLlm\nconst aiText = item.json.text || '';\n\n// Parse JSON from AI response\nlet analysis = {};\ntry {\n  analysis = JSON.parse(aiText);\n} catch(e) {\n  const match = aiText.match(/\\{[\\s\\S]*\\}/);\n  if (match) {\n    try { analysis = JSON.parse(match[0]); } catch(e2) {}\n  }\n}\n\n// Apply defaults\nif (!analysis.email_type)               analysis.email_type = 'other';\nif (!analysis.priority)                 analysis.priority = 'medium';\nif (analysis.needs_reply === undefined) analysis.needs_reply = false;\nif (analysis.is_meeting === undefined)  analysis.is_meeting = false;\nif (!analysis.reply_text)              analysis.reply_text = '';\nif (!analysis.meeting_date)            analysis.meeting_date = '';\nif (!analysis.meeting_time)            analysis.meeting_time = '10:00';\nif (!analysis.meeting_topic)           analysis.meeting_topic = '';\nif (!analysis.reason)                  analysis.reason = '';\n\n// Email metadata from Normalize Email\nconst subject   = email.subject || '(no subject)';\nconst fromEmail = email.from || '';\nconst fromName  = email.fromName || '';\n\n// Build datetime for Google Calendar in Europe/Berlin (DST-aware, 1 hour event)\nconst TZ = 'Europe/Berlin';\nlet meetingStart = '';\nlet meetingEnd   = '';\nlet meetingStartHuman = '';\nlet meetingAltHuman   = '';\nif (analysis.is_meeting && analysis.meeting_date) {\n  const time = analysis.meeting_time || '10:00';\n  const startDT = DateTime.fromISO(analysis.meeting_date + 'T' + time, { zone: TZ });\n  if (startDT.isValid) {\n    meetingStart = startDT.toISO();\n    meetingEnd   = startDT.plus({ hours: 1 }).toISO();\n    meetingStartHuman = startDT.toFormat('dd.MM.yyyy HH:mm');\n    meetingAltHuman   = startDT.plus({ days: 1 }).toFormat('dd.MM.yyyy HH:mm');\n  }\n}\n\n// Downgrade is_meeting if AI found no specific date/time\nif (!meetingStart) analysis.is_meeting = false;\n\n// Human-readable action for log\nconst actionParts = [];\nif (analysis.is_meeting)  actionParts.push('Calendar Event');\nif (analysis.needs_reply) actionParts.push(String(analysis.priority).toLowerCase() === 'high' ? 'Reply Sent' : 'Draft Reply');\nif (!actionParts.length)  actionParts.push('Ignored');\nconst action = actionParts.join(' + ');\n\nreturn [{\n  json: {\n    emailId:       email.emailId,\n    threadId:      email.threadId || '',\n    subject,\n    from:          fromEmail,\n    fromName,\n    emailBody:     email.body || '',\n    email_type:    analysis.email_type,\n    priority:      analysis.priority,\n    needs_reply:   analysis.needs_reply === true,\n    is_meeting:    analysis.is_meeting  === true,\n    reply_text:    analysis.reply_text,\n    meeting_topic: analysis.meeting_topic || subject,\n    meeting_date:  analysis.meeting_date,\n    meeting_time:  analysis.meeting_time,\n    meeting_start: meetingStart,\n    meeting_end:   meetingEnd,\n    meeting_start_human: meetingStartHuman,\n    meeting_alt_human:   meetingAltHuman,\n    reason:        analysis.reason,\n    reply_subject: 'Re: ' + subject,\n    action,\n  }\n}];"
      },
      "id": "code_parse",
      "name": "Parse AI Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -256,
        304
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SHEET_ID",
          "cachedResultName": "Project 3.1 Sheets Test"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Email Log",
          "cachedResultName": "Email Log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Timestamp": "={{ $now.setZone('Europe/Berlin').toISO({ suppressMilliseconds: true }) }}",
            "From": "={{ $json.from }}",
            "From Name": "={{ $json.fromName }}",
            "Subject": "={{ $json.subject }}",
            "Type": "={{ $json.email_type }}",
            "Priority": "={{ $json.priority }}",
            "Needs Reply": "={{ $json.needs_reply ? 'Yes' : 'No' }}",
            "Is Meeting": "={{ $json.is_meeting ? 'Yes' : 'No' }}",
            "Action": "={{ $json.action }}",
            "Reason": "={{ $json.reason }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Timestamp",
              "displayName": "Timestamp",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "From",
              "displayName": "From",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "From Name",
              "displayName": "From Name",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Subject",
              "displayName": "Subject",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Type",
              "displayName": "Type",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Priority",
              "displayName": "Priority",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Needs Reply",
              "displayName": "Needs Reply",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Is Meeting",
              "displayName": "Is Meeting",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Action",
              "displayName": "Action",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Reason",
              "displayName": "Reason",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string",
              "removed": false
            }
          ]
        },
        "options": {}
      },
      "id": "sheets_log",
      "name": "Log to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        144,
        304
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "cond_meeting",
              "leftValue": "={{ $json.is_meeting }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if_meeting",
      "name": "IF Meeting",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        352,
        160
      ]
    },
    {
      "parameters": {
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "you@example.com",
          "cachedResultName": "you@example.com"
        },
        "start": "={{ $('Parse AI Response').item.json.meeting_start }}",
        "end": "={{ $('Parse AI Response').item.json.meeting_end }}",
        "additionalFields": {
          "description": "=From: {{ $('Parse AI Response').item.json.fromName }} <{{ $('Parse AI Response').item.json.from }}>\nSubject: {{ $('Parse AI Response').item.json.subject }}\n\nAI Note: {{ $('Parse AI Response').item.json.reason }}",
          "summary": "={{ $('Parse AI Response').item.json.meeting_topic }}"
        }
      },
      "id": "calendar_event",
      "name": "Create Calendar Event",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        1008,
        -208
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "cond_reply",
              "leftValue": "={{ $json.needs_reply }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if_reply",
      "name": "IF Reply Needed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        352,
        480
      ]
    },
    {
      "parameters": {
        "resource": "draft",
        "subject": "={{ $json.reply_subject }}",
        "message": "={{ $json.reply_text }}",
        "options": {
          "threadId": "={{ $json.threadId }}",
          "sendTo": "={{ $json.from }}"
        }
      },
      "id": "gmail_draft",
      "name": "Create Gmail Draft",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        1024,
        464
      ],
      "credentials": {}
    },
    {
      "parameters": {},
      "id": "noop_ignore",
      "name": "Ignore Email",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        704,
        256
      ]
    },
    {
      "parameters": {
        "jsCode": "const e = $input.first().json;\n\n// Headers: handle both simplified object and raw array formats\nconst headersObj = (e.headers && !Array.isArray(e.headers)) ? e.headers : {};\nconst headersArr = (e.payload && Array.isArray(e.payload.headers)) ? e.payload.headers : [];\nconst getHeader = (name) => {\n  const lname = name.toLowerCase();\n  if (headersObj[lname]) return headersObj[lname];\n  const h = headersArr.find(x => (x.name || '').toLowerCase() === lname);\n  return h ? h.value : '';\n};\n\n// Body: prefer decoded text, then raw payload parts, then snippet\nfunction decode(data) {\n  try { return Buffer.from(data, 'base64').toString('utf-8'); } catch (err) { return ''; }\n}\nfunction extractBody(payload) {\n  if (!payload) return '';\n  if (payload.mimeType === 'text/plain' && payload.body && payload.body.data) return decode(payload.body.data);\n  if (payload.parts && payload.parts.length) {\n    for (const p of payload.parts) {\n      if (p.mimeType === 'text/plain' && p.body && p.body.data) return decode(p.body.data);\n    }\n    for (const p of payload.parts) {\n      const r = extractBody(p);\n      if (r) return r;\n    }\n  }\n  if (payload.body && payload.body.data) return decode(payload.body.data);\n  return '';\n}\nlet body = e.text || e.textPlain || '';\nif (!body && e.payload) body = extractBody(e.payload);\nif (!body) body = e.snippet || '';\n\n// Sender\nlet fromRaw = '';\nlet fromName = '';\nif (e.from && e.from.value && e.from.value[0]) {\n  fromRaw = e.from.value[0].address || '';\n  fromName = e.from.value[0].name || '';\n} else if (typeof e.from === 'string' && e.from) {\n  fromRaw = e.from;\n} else {\n  fromRaw = getHeader('From');\n}\nconst m = String(fromRaw).match(/^\\s*\"?([^\"<]*?)\"?\\s*<(.+?)>\\s*$/);\nlet fromEmail = fromRaw;\nif (m) {\n  if (!fromName) fromName = m[1].trim();\n  fromEmail = m[2].trim();\n}\n\nconst subject = e.subject || getHeader('Subject') || '(no subject)';\nconst date = e.date || getHeader('Date') || '';\n\nreturn [{\n  json: {\n    emailId: e.id || '',\n    threadId: e.threadId || '',\n    subject,\n    from: fromEmail,\n    fromName,\n    date,\n    body,\n  }\n}];"
      },
      "id": "code_normalize",
      "name": "Normalize Email",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -736,
        416
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "rule_high",
                    "leftValue": "={{ $json.priority }}",
                    "rightValue": "high",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "High → Send"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 3
                },
                "conditions": [
                  {
                    "id": "rule_other",
                    "leftValue": "={{ $json.priority }}",
                    "rightValue": "high",
                    "operator": {
                      "type": "string",
                      "operation": "notEquals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Medium/Low → Draft"
            }
          ]
        },
        "options": {}
      },
      "id": "switch_priority",
      "name": "Route by Priority",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        704,
        400
      ]
    },
    {
      "parameters": {
        "operation": "reply",
        "messageId": "={{ $json.emailId }}",
        "emailType": "text",
        "message": "={{ $json.reply_text }}",
        "options": {
          "replyToSenderOnly": true
        }
      },
      "id": "gmail_send",
      "name": "Send Email Now",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        1024,
        288
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "jsCode": "const item = $input.first();\nconst raw = item.json;\nconst binary = item.binary || {};\nconst clean = (s) => String(s || '').replace(/[^\\w\\s.-]/g, '').trim();\n\n// Read sender directly from Gmail Trigger output (runs in parallel with Normalize Email)\nlet fromEmail = '';\nlet fromName = '';\nif (raw.from && raw.from.value && raw.from.value[0]) {\n  fromEmail = raw.from.value[0].address || '';\n  fromName = raw.from.value[0].name || '';\n} else if (typeof raw.from === 'string') {\n  fromEmail = raw.from;\n}\nconst subject = raw.subject || '(no subject)';\nconst date = raw.date || '';\nconst stamp = (date && date.length >= 10) ? date.slice(0, 10) : new Date().toISOString().slice(0, 10);\n\nconst cleanFromName = clean(fromName) || 'sender';\nconst clientFolderName = cleanFromName !== 'sender'\n  ? cleanFromName\n  : clean(fromEmail.split('@')[0]) || 'Unknown Sender';\n\nconst out = [];\nfor (const key of Object.keys(binary)) {\n  const b = binary[key];\n  const origName = b.fileName || key;\n  out.push({\n    json: {\n      fileName: stamp + '_' + cleanFromName + '_' + origName,\n      originalName: origName,\n      mimeType: b.mimeType || '',\n      from: fromEmail,\n      subject,\n      clientFolderName,\n    },\n    binary: { data: b },\n  });\n}\nreturn out;"
      },
      "id": "code_attachments",
      "name": "Extract Attachments",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -800,
        -48
      ]
    },
    {
      "parameters": {
        "name": "={{ $json.fileName }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive",
          "cachedResultName": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.folderId }}",
          "cachedResultName": "Email Attachments"
        },
        "options": {}
      },
      "id": "drive_upload",
      "name": "Upload to Drive",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        32,
        0
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "chatId": "=YOUR_CHAT_ID",
        "text": "=Ошибка: <code>{{ $json.execution.error.message }}</code> \nWorkflow: <a href=\"{{ $json.execution.url }}\">{{ $json.workflow.name }}</a> \nNode: {{ $json.execution.lastNodeExecuted }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        64,
        -240
      ],
      "id": "0d073220-2c06-4b60-ac7a-c047fc28d219",
      "name": "Send a text message2",
      "credentials": {}
    },
    {
      "parameters": {
        "jsCode": "const j = $input.first().json;\n\n// Only high priority\nif (j.priority !== 'high') return [];\n\n// Only during working hours: Mon-Fri 09:00-18:00 Europe/Berlin\nconst now = DateTime.now().setZone('Europe/Berlin');\nconst isWorkday = now.weekday >= 1 && now.weekday <= 5;\nconst isWorkHour = now.hour >= 9 && now.hour < 18;\n\nif (isWorkday && isWorkHour) {\n  return [{ json: j }];\n}\nreturn [];"
      },
      "id": "code_urgent_filter",
      "name": "Urgent Work-Hours Filter",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -64,
        560
      ]
    },
    {
      "parameters": {
        "chatId": "YOUR_CHAT_ID",
        "text": "=🚨 СРОЧНО — письмо высокого приоритета!\n\nОт: {{ $json.fromName }} <{{ $json.from }}>\nТема: {{ $json.subject }}\n\n{{ $json.reason }}\n\n⚡ Рекомендуется срочный звонок.",
        "additionalFields": {}
      },
      "id": "telegram_alert",
      "name": "Send Telegram Alert",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        208,
        624
      ],
      "credentials": {}
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.errorTrigger",
      "typeVersion": 1,
      "position": [
        -192,
        -240
      ],
      "id": "020ca5a7-23bf-4cad-b788-3fa78cd1ee06",
      "name": "Error Trigger"
    },
    {
      "parameters": {
        "content": "### Gmail Trigger\nКаждую минуту проверяет почту и запускает процесс при новых непрочитанных письмах (с вложениями).",
        "height": 110,
        "width": 220,
        "color": 4
      },
      "id": "36e29e63-193f-4b75-a120-5f90e5628934",
      "name": "Note: Gmail Trigger",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1136,
        192
      ]
    },
    {
      "parameters": {
        "content": "### Normalize Email\nИзвлекает отправителя, тему, дату и текст письма из любого формата Gmail; декодирует base64.",
        "height": 110,
        "width": 220,
        "color": 6
      },
      "id": "5400d21a-dc55-4071-8f27-9326b7a7a27c",
      "name": "Note: Normalize Email",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -832,
        304
      ]
    },
    {
      "parameters": {
        "content": "### Extract Attachments\nРазбивает вложения по одному и формирует имя файла (дата_отправитель_имя) для загрузки в Drive.",
        "height": 110,
        "width": 220,
        "color": 6
      },
      "id": "1fdc6eb6-8927-4de1-8cb3-48698736b192",
      "name": "Note: Extract Attachments",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -928,
        -160
      ]
    },
    {
      "parameters": {
        "content": "### Analyze Email\nAI-анализ письма (GPT-4.1-mini): тип, приоритет, нужен ли ответ/встреча → строгий JSON.",
        "height": 110,
        "width": 220,
        "color": 5
      },
      "id": "561542fb-6436-4110-995c-abec2c137f90",
      "name": "Note: Analyze Email",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -496,
        224
      ]
    },
    {
      "parameters": {
        "content": "### OpenRouter Chat Model\nЯзыковая модель GPT-4.1-mini (OpenRouter) для ноды анализа письма.",
        "height": 126,
        "width": 204,
        "color": 5
      },
      "id": "082aa47d-cb7e-4620-881d-3818ff9856e0",
      "name": "Note: OpenRouter Chat Model",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -448,
        496
      ]
    },
    {
      "parameters": {
        "content": "### Parse AI Response\nПарсит JSON от AI, ставит значения по умолчанию, считает время встречи (Europe/Berlin) и раздаёт данные по всем веткам.",
        "height": 110,
        "width": 220,
        "color": 6
      },
      "id": "f7a7aefb-84c1-48b3-964c-52e8db5a6f78",
      "name": "Note: Parse AI Response",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -256,
        192
      ]
    },
    {
      "parameters": {
        "content": "### Upload to Drive\nЗагружает вложение в подпапку клиента (имя отправителя) внутри «Email Attachments» на Google Drive.",
        "height": 94,
        "width": 172,
        "color": 3
      },
      "id": "bb1bd9e6-5e82-4f8d-83e3-47a5ba2f9069",
      "name": "Note: Upload to Drive",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        128,
        -64
      ]
    },
    {
      "parameters": {
        "content": "### Log to Google Sheets\nЗаписывает каждое письмо в лист «Email Log»: дата, отправитель, тема, тип, приоритет, действие.",
        "height": 110,
        "width": 220,
        "color": 3
      },
      "id": "5103bbbf-4045-40b1-8279-accb54deef82",
      "name": "Note: Log to Google Sheets",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        320,
        320
      ]
    },
    {
      "parameters": {
        "content": "### IF Meeting\nЕсли письмо про встречу (is_meeting = true) → проверка слота (Check Availability): свободно → событие + подтверждение, занято → предложение другого времени.",
        "height": 110,
        "width": 220,
        "color": 7
      },
      "id": "fe986825-426a-4eb3-b3e4-dfd7fd05d388",
      "name": "Note: IF Meeting",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        224,
        48
      ]
    },
    {
      "parameters": {
        "content": "### Create Calendar Event\nСоздаёт событие в Google Calendar (1 час, Europe/Berlin) по дате и теме встречи из письма.",
        "height": 110,
        "width": 220,
        "color": 2
      },
      "id": "480f064d-426a-43c7-bc71-b40b87f86190",
      "name": "Note: Create Calendar Event",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1008,
        -320
      ]
    },
    {
      "parameters": {
        "content": "### IF Reply Needed\nЕсли письмо требует ответа (needs_reply = true) → маршрутизация по приоритету; иначе игнор.",
        "height": 110,
        "width": 220,
        "color": 7
      },
      "id": "6736cc38-80be-4190-bb97-889e07d7cd9f",
      "name": "Note: IF Reply Needed",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        400,
        608
      ]
    },
    {
      "parameters": {
        "content": "### Route by Priority\nhigh → мгновенная отправка ответа; medium/low → создание черновика.",
        "height": 110,
        "width": 220,
        "color": 7
      },
      "id": "96cfab84-1c99-4536-bb36-e218bb0150bf",
      "name": "Note: Route by Priority",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        656,
        544
      ]
    },
    {
      "parameters": {
        "content": "### Send Email Now\nСразу отвечает в ветку исходного письма (только отправителю) - для срочных писем высокого приоритета (high).",
        "height": 110,
        "width": 220,
        "color": 2
      },
      "id": "a3200897-10e6-44c5-9df8-47e1a8df1a5d",
      "name": "Note: Send Email Now",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1024,
        176
      ]
    },
    {
      "parameters": {
        "content": "### Create Gmail Draft\nСоздаёт черновик ответа в ветке исходного письма с адресом отправителя - для писем среднего/низкого приоритета.",
        "height": 110,
        "width": 220,
        "color": 2
      },
      "id": "c8a51c49-50ce-4983-adf2-4e1b3c5daf23",
      "name": "Note: Create Gmail Draft",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1152,
        528
      ]
    },
    {
      "parameters": {
        "content": "### Ignore Email\nЗаглушка (No-Op): письмо не требует действий - ветка «игнорировать».",
        "height": 110,
        "width": 220
      },
      "id": "c620280a-b715-4253-8c2a-1dab76df116a",
      "name": "Note: Ignore Email",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        576,
        144
      ]
    },
    {
      "parameters": {
        "content": "### Urgent Work-Hours Filter\nПропускает дальше только срочные письма (high) в рабочие часы (Пн–Пт 09:00–18:00, Берлин).",
        "height": 110,
        "width": 220,
        "color": 6
      },
      "id": "ceedb045-0386-44b8-9002-af30912793e6",
      "name": "Note: Urgent Work-Hours Filter",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -160,
        720
      ]
    },
    {
      "parameters": {
        "content": "### Send Telegram Alert\nTelegram-уведомление о срочном письме с рекомендацией перезвонить.",
        "height": 110,
        "width": 220,
        "color": 2
      },
      "id": "db7bdaf3-1cc7-4bf2-a18d-e0459b1b80cd",
      "name": "Note: Send Telegram Alert",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        144,
        800
      ]
    },
    {
      "parameters": {
        "content": "### Error Trigger\nСрабатывает при сбое выполнения воркфлоу.",
        "height": 110,
        "width": 220,
        "color": 4
      },
      "id": "fc9bae91-9d50-4f9c-9fa8-de9ce052bf75",
      "name": "Note: Error Trigger",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -224,
        -352
      ]
    },
    {
      "parameters": {
        "content": "### Telegram: ошибка\nОтправляет в Telegram уведомление о технической ошибке воркфлоу (текст ошибки, нода, ссылка).",
        "height": 110,
        "width": 220,
        "color": 2
      },
      "id": "a0f6985c-1233-433c-a8a3-0954cc09d362",
      "name": "Note: Telegram Error",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        64,
        -352
      ]
    },
    {
      "parameters": {
        "content": "## AI Email Assistant  «Умный секретарь»\n**Автор:** Денис Копыл · группа 270426-aim   |   **Дата:** 30.09.2026   |   **Версия:** 1.4 (сдана версия 1.3)\n\nАвтоматически читает входящие письма Gmail, классифицирует их через AI, готовит ответ или черновик, заводит встречи в Google Calendar (с проверкой занятости слота), ведёт лог в Sheets, сохраняет вложения в Drive по папкам клиентов и шлёт Telegram-алерты по срочным письмам.\n\n**v1.4:** ответы и черновики в ветке исходного письма, время в логе в ISO-8601 (Берлин), явная модель, уточнённое поле Action.",
        "height": 210,
        "width": 760,
        "color": 5
      },
      "id": "7c5087c8-1e55-410c-bd8a-57d458d933de",
      "name": "Note: Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1072,
        -336
      ]
    },
    {
      "parameters": {
        "content": "### AI: Analyze Email - промпт и модель\n**Модель:** `openai/gpt-4.1-mini` через OpenRouter\n**Параметры:** по умолчанию (temperature явно не задана)\n\n**Системный промпт:**\nYou are an intelligent email assistant – a smart secretary. Analyze the provided email and return ONLY a valid JSON object (no markdown, no code blocks, no extra text) with these exact fields:\n- email_type: question/information/meeting/advertisement/work/personal/other\n- priority: high/medium/low\n- needs_reply: boolean\n- is_meeting: boolean\n- reply_text: проф. ответ на языке письма (иначе \"\")\n- meeting_date: YYYY-MM-DD / \"\"\n- meeting_time: HH:MM (24h, дефолт 10:00)\n- meeting_topic: тема встречи / \"\"\n- reason: одно предложение (≤120 симв.)",
        "height": 390,
        "width": 520
      },
      "id": "f725f81d-1ea4-476d-824d-a246a7a1f4cf",
      "name": "Note: AI Prompt & Model",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1344,
        496
      ]
    },
    {
      "parameters": {
        "resource": "calendar",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "you@example.com",
          "cachedResultName": "you@example.com"
        },
        "timeMin": "={{ $('Parse AI Response').item.json.meeting_start }}",
        "timeMax": "={{ $('Parse AI Response').item.json.meeting_end }}",
        "options": {
          "outputFormat": "availability"
        }
      },
      "id": "694b74aa-ab8a-472c-a6b2-067172d9d376",
      "name": "Check Availability",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        560,
        -128
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "cond_free",
              "leftValue": "={{ $json.available }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "7f5b4b9b-99b5-4c37-9a8a-b0c635a63293",
      "name": "IF Slot Free",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        768,
        -128
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $('Parse AI Response').item.json.from }}",
        "subject": "={{ $('Parse AI Response').item.json.reply_subject }}",
        "emailType": "text",
        "message": "={{ 'Здравствуйте! К сожалению, предложенное время ' + $('Parse AI Response').item.json.meeting_start_human + ' уже занято. Предлагаю перенести встречу на ' + $('Parse AI Response').item.json.meeting_alt_human + ' — или сообщите удобное вам время, и я подтвержу.\\n\\nС уважением.' }}",
        "options": {}
      },
      "id": "a05c0d7f-277b-4ae0-9489-642ba2617c2b",
      "name": "Send Reschedule Reply",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        1008,
        -16
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "content": "### Проверка занятости (улучшение)\nПеред созданием события проверяет свободен ли слот.\n• Свободно → Create Calendar Event\n• Занято → Send Reschedule Reply (ответ «предложите другое время» + альтернатива)",
        "height": 150,
        "width": 380,
        "color": 3
      },
      "id": "b9eace8f-49bf-4f28-a206-bb766868bde5",
      "name": "Note: Availability Check",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        480,
        -288
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $('Parse AI Response').item.json.from }}",
        "subject": "={{ $('Parse AI Response').item.json.reply_subject }}",
        "emailType": "text",
        "message": "={{ 'Здравствуйте! Подтверждаю встречу «' + $('Parse AI Response').item.json.meeting_topic + '» — ' + $('Parse AI Response').item.json.meeting_start_human + '. Событие добавлено в календарь. С уважением.' }}",
        "options": {}
      },
      "id": "9a17917f-e84e-4ba4-971b-21b3cf81bda0",
      "name": "Send Confirmation",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        1280,
        -208
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "content": "### Send Confirmation\nПосле создания события шлёт клиенту письмо-подтверждение брони (тема и время встречи).",
        "height": 110,
        "width": 220,
        "color": 2
      },
      "id": "ca749793-fbb5-41fd-a4c9-ae8093882cef",
      "name": "Note: Send Confirmation",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1280,
        -320
      ]
    },
    {
      "parameters": {
        "resource": "folder",
        "name": "={{ $json.clientFolderName }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive",
          "cachedResultName": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_DRIVE_ID",
          "cachedResultName": "Email Attachments"
        },
        "options": {}
      },
      "id": "696fd9e2-3f31-48fd-b273-c4cb7ea555a6",
      "name": "Create Client Folder",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        -512,
        0
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "jsCode": "const results = [];\nconst allItems = $input.all();\nconst allExtractItems = $('Extract Attachments').all();\n\nfor (let i = 0; i < allItems.length; i++) {\n  const folderId = allItems[i].json.id;\n  const extractItem = allExtractItems[i] || allExtractItems[0];\n  results.push({\n    json: {\n      folderId,\n      fileName: extractItem.json.fileName,\n      mimeType: extractItem.json.mimeType,\n    },\n    binary: extractItem.binary,\n  });\n}\nreturn results;"
      },
      "id": "a85b213e-7384-4454-9449-98b5db74ba0c",
      "name": "Reassemble Attachment",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        0
      ]
    },
    {
      "parameters": {
        "content": "### Create Client Folder\nСоздаёт подпапку с именем отправителя внутри «Email Attachments» на Google Drive для сортировки вложений по клиентам.",
        "height": 110,
        "width": 220,
        "color": 3
      },
      "id": "e11a35c4-0848-4125-96ca-23b233d65357",
      "name": "Note: Create Client Folder",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -512,
        -112
      ]
    },
    {
      "parameters": {
        "content": "### Reassemble Attachment\nВосстанавливает связку: ID подпапки (из Drive) + бинарные данные (из Extract Attachments) для загрузки в подпапку.",
        "height": 80,
        "width": 204,
        "color": 6
      },
      "id": "66366e71-a966-46bf-8b04-527d10960278",
      "name": "Note: Reassemble Attachment",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -224,
        -80
      ]
    },
    {
      "parameters": {
        "operation": "markAsRead",
        "messageId": "={{ $json.emailId }}"
      },
      "id": "67493451-d989-43ef-9ae9-4115f35fd6a6",
      "name": "Mark as Read",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        -736,
        624
      ],
      "credentials": {}
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "cond_is_reply",
              "leftValue": "={{ $json.subject }}",
              "rightValue": "Re:",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "824384ec-6b89-49f2-8201-72e91ab3d7c4",
      "name": "Skip Replies",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -656,
        176
      ]
    },
    {
      "parameters": {
        "content": "### Skip Replies\nПропускает письма-ответы (Re:) — воркфлоу не обрабатывает свои же автоответы повторно.",
        "height": 122,
        "width": 220,
        "color": 7
      },
      "id": "702c109b-3173-407e-8ae1-284d2362d853",
      "name": "Note: Skip Replies",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -864,
        96
      ]
    },
    {
      "parameters": {
        "content": "### Mark as Read\nПомечает письмо прочитанным, чтобы Gmail Trigger не забирал его повторно при следующем опросе.",
        "height": 110,
        "width": 220,
        "color": 2
      },
      "id": "51f22fcd-82a2-4d83-b4e3-5fae80e458d1",
      "name": "Note: Mark as Read",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -800,
        752
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze Email",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Email": {
      "main": [
        [
          {
            "node": "Parse AI Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Response": {
      "main": [
        [
          {
            "node": "Log to Google Sheets",
            "type": "main",
            "index": 0
          },
          {
            "node": "IF Meeting",
            "type": "main",
            "index": 0
          },
          {
            "node": "IF Reply Needed",
            "type": "main",
            "index": 0
          },
          {
            "node": "Urgent Work-Hours Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Meeting": {
      "main": [
        [
          {
            "node": "Check Availability",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ignore Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Reply Needed": {
      "main": [
        [
          {
            "node": "Route by Priority",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ignore Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail Trigger": {
      "main": [
        [
          {
            "node": "Normalize Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Extract Attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Email": {
      "main": [
        [
          {
            "node": "Mark as Read",
            "type": "main",
            "index": 0
          },
          {
            "node": "Skip Replies",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Priority": {
      "main": [
        [
          {
            "node": "Send Email Now",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Gmail Draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Urgent Work-Hours Filter": {
      "main": [
        [
          {
            "node": "Send Telegram Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Trigger": {
      "main": [
        [
          {
            "node": "Send a text message2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Availability": {
      "main": [
        [
          {
            "node": "IF Slot Free",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Slot Free": {
      "main": [
        [
          {
            "node": "Create Calendar Event",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Reschedule Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Calendar Event": {
      "main": [
        [
          {
            "node": "Send Confirmation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Attachments": {
      "main": [
        [
          {
            "node": "Create Client Folder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Client Folder": {
      "main": [
        [
          {
            "node": "Reassemble Attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reassemble Attachment": {
      "main": [
        [
          {
            "node": "Upload to Drive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip Replies": {
      "main": [
        [],
        [
          {
            "node": "Analyze Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": true
  },
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodeGroups": [],
  "staticData": null
}