AutomationFlowsAI & RAG › AI Chatbot for Telegram & Gmail

AI Chatbot for Telegram & Gmail

Original n8n title: AI Agent Workflow

AI Agent Workflow. Uses telegramTrigger, chatTrigger, telegram, openAi. Event-driven trigger; 82 nodes.

Event trigger★★★★★ complexityAI-powered82 nodesTelegram TriggerChat TriggerTelegramOpenAIHTTP RequestGmailGoogle CalendarNotion
AI & RAG Trigger: Event Nodes: 82 Complexity: ★★★★★ AI nodes: yes Added:

This workflow follows the Agent → Chat Trigger 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
{
  "name": "AI Agent Workflow",
  "nodes": [
    {
      "parameters": {
        "updates": [
          "*"
        ],
        "additionalFields": {}
      },
      "id": "single-92ae6fd3-520c-4f48-a369-24305b23efad",
      "name": "Telegram Trigger",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1,
      "position": [
        -1680,
        1296
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "typeVersion": 1.4,
      "position": [
        -1680,
        1488
      ],
      "id": "single-806479e3-7e80-4d0f-a4ec-6ec375a0de91",
      "name": "Chat Trigger"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "msg",
              "name": "message_text",
              "value": "={{ ($json.message && ($json.message.text || $json.message.caption)) || $json.chatInput || '' }}",
              "type": "string"
            },
            {
              "id": "uid",
              "name": "user_id",
              "value": "={{ ($json.message && $json.message.from && $json.message.from.id) || $json.sessionId || 'unknown' }}",
              "type": "string"
            },
            {
              "id": "uname",
              "name": "username",
              "value": "={{ ($json.message && $json.message.from && ($json.message.from.username || $json.message.from.first_name)) || 'the user' }}",
              "type": "string"
            },
            {
              "id": "chat",
              "name": "chat_id",
              "value": "={{ ($json.message && $json.message.chat && $json.message.chat.id) || '' }}",
              "type": "string"
            },
            {
              "id": "uup",
              "name": "update_id",
              "value": "={{ $json.update_id || '' }}",
              "type": "string"
            },
            {
              "id": "hv",
              "name": "has_voice",
              "value": "={{ !!($json.message && $json.message.voice) }}",
              "type": "boolean"
            },
            {
              "id": "vfid",
              "name": "voice_file_id",
              "value": "={{ ($json.message && $json.message.voice && $json.message.voice.file_id) || '' }}",
              "type": "string"
            },
            {
              "id": "hp",
              "name": "has_photo",
              "value": "={{ Array.isArray($json.message && $json.message.photo) && $json.message.photo.length > 0 }}",
              "type": "boolean"
            },
            {
              "id": "pfid",
              "name": "photo_file_id",
              "value": "={{ Array.isArray($json.message && $json.message.photo) ? $json.message.photo[$json.message.photo.length - 1].file_id : '' }}",
              "type": "string"
            },
            {
              "id": "src",
              "name": "source",
              "value": "={{ $json.message ? 'telegram' : 'chat' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "single-48eb5a1b-ae95-425f-8083-1e3d54ed06b3",
      "name": "Normalize Input",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1456,
        1296
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $input.first().json || {};\nconst allowedIds = String(process.env.TELEGRAM_ALLOWED_CHAT_IDS || 'YOUR_TELEGRAM_CHAT_ID').split(',').map((id) => id.trim()).filter(Boolean);\nconst source = data.source || (data.chat_id ? 'telegram' : 'chat');\nconst userId = String(data.user_id || '');\nconst chatId = String(data.chat_id || '');\nconst isInteractiveChat = source === 'chat' || userId === 'unknown';\nconst isAllowedTelegram = allowedIds.includes(userId) || allowedIds.includes(chatId);\n\nif (!isInteractiveChat && !isAllowedTelegram) {\n  return [];\n}\n\nconst staticData = $getWorkflowStaticData('global');\nconst seen = staticData.seenUpdates || [];\nconst uid = String(data.update_id || '');\nconst shouldDedup = source === 'telegram' && uid;\n\nif (shouldDedup && seen.includes(uid)) {\n  return [];\n}\n\nif (shouldDedup) {\n  seen.push(uid);\n  if (seen.length > 200) seen.shift();\n  staticData.seenUpdates = seen;\n}\n\nreturn [{ json: data }];"
      },
      "id": "single-guard-001",
      "name": "Whitelist + Dedup",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1232,
        1296
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "v",
                    "leftValue": "={{ $json.has_voice }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "voice"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "p",
                    "leftValue": "={{ $json.has_photo }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "photo"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "t",
                    "leftValue": "={{ $json.message_text }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "notEmpty",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "text"
            }
          ]
        },
        "options": {}
      },
      "id": "single-input-router-001",
      "name": "Input Type Router",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.3,
      "position": [
        -1008,
        1280
      ]
    },
    {
      "parameters": {
        "resource": "file",
        "fileId": "={{ $json.voice_file_id }}",
        "additionalFields": {}
      },
      "id": "single-8285ad4c-f142-4925-8f38-8d839db0a898",
      "name": "Get Voice File",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -784,
        1104
      ]
    },
    {
      "parameters": {
        "resource": "audio",
        "operation": "transcribe",
        "options": {}
      },
      "id": "single-2e0147af-51eb-4d13-9fb2-399b69723295",
      "name": "Transcribe Voice",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.6,
      "position": [
        -560,
        1104
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "vt",
              "name": "message_text",
              "value": "={{ $json.text }}",
              "type": "string"
            },
            {
              "id": "vid",
              "name": "user_id",
              "value": "={{ $('Whitelist + Dedup').item.json.user_id }}",
              "type": "string"
            },
            {
              "id": "vun",
              "name": "username",
              "value": "={{ $('Whitelist + Dedup').item.json.username }}",
              "type": "string"
            },
            {
              "id": "vci",
              "name": "chat_id",
              "value": "={{ $('Whitelist + Dedup').item.json.chat_id }}",
              "type": "string"
            },
            {
              "id": "vhv",
              "name": "input_was_voice",
              "value": "={{ true }}",
              "type": "boolean"
            },
            {
              "id": "vit",
              "name": "input_type",
              "value": "voice",
              "type": "string"
            },
            {
              "id": "fvr",
              "name": "force_voice_reply",
              "value": "={{ true }}",
              "type": "boolean"
            }
          ]
        },
        "options": {}
      },
      "id": "single-07cdf726-13e0-46b8-a735-d0162e1481bc",
      "name": "Prepare Voice Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -336,
        1104
      ]
    },
    {
      "parameters": {
        "resource": "file",
        "fileId": "={{ $json.photo_file_id }}",
        "additionalFields": {}
      },
      "id": "single-photo-get-001",
      "name": "Get Photo File",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -784,
        1296
      ]
    },
    {
      "parameters": {
        "resource": "image",
        "operation": "analyze",
        "modelId": {
          "__rl": true,
          "value": "gpt-4o-mini",
          "mode": "list"
        },
        "text": "=Beschreibe was auf dem Bild zu sehen ist. Wenn es eine Visitenkarte, ein Whiteboard, ein Dokument oder ein Screenshot ist, extrahiere alle relevanten Texte und Daten strukturiert. User-Bildunterschrift: {{ $('Whitelist + Dedup').item.json.message_text || '(keine)' }}",
        "options": {}
      },
      "id": "single-vision-001",
      "name": "Analyze Photo",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.6,
      "position": [
        -560,
        1296
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "pt",
              "name": "message_text",
              "value": "=Bild Analyse: {{ $json.content || $json.text || JSON.stringify($json) }}\n\nUser Caption: {{ $('Whitelist + Dedup').item.json.message_text }}",
              "type": "string"
            },
            {
              "id": "pid",
              "name": "user_id",
              "value": "={{ $('Whitelist + Dedup').item.json.user_id }}",
              "type": "string"
            },
            {
              "id": "pun",
              "name": "username",
              "value": "={{ $('Whitelist + Dedup').item.json.username }}",
              "type": "string"
            },
            {
              "id": "pci",
              "name": "chat_id",
              "value": "={{ $('Whitelist + Dedup').item.json.chat_id }}",
              "type": "string"
            },
            {
              "id": "phv",
              "name": "input_was_voice",
              "value": "={{ false }}",
              "type": "boolean"
            }
          ]
        },
        "options": {}
      },
      "id": "single-photo-prep-001",
      "name": "Prepare Photo Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -336,
        1296
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "tt",
              "name": "message_text",
              "value": "={{ $json.message_text }}",
              "type": "string"
            },
            {
              "id": "tid",
              "name": "user_id",
              "value": "={{ $json.user_id }}",
              "type": "string"
            },
            {
              "id": "tun",
              "name": "username",
              "value": "={{ $json.username }}",
              "type": "string"
            },
            {
              "id": "tci",
              "name": "chat_id",
              "value": "={{ $json.chat_id }}",
              "type": "string"
            },
            {
              "id": "thv",
              "name": "input_was_voice",
              "value": "={{ false }}",
              "type": "boolean"
            }
          ]
        },
        "options": {}
      },
      "id": "single-text-prep-001",
      "name": "Prepare Text Input",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -336,
        1488
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $input.first().json || {};\nconst staticData = $getWorkflowStaticData('global');\nconst raw = (data.message_text || '').trim();\nconst lower = raw.toLowerCase().replace(/\\s+/g, ' ').trim();\nconst commandName = (lower.match(/^\\/([a-z0-9_]+)/i) || [])[1] || '';\nfunction commandIs(...aliases) {\n  return aliases.some((alias) => {\n    const normalized = String(alias || '').toLowerCase().replace(/\\s+/g, ' ').trim();\n    return lower === normalized || lower.startsWith(normalized + ' ');\n  });\n}\nfunction commandStarts(...names) {\n  return names.includes(commandName);\n}\nfunction commandText(name) {\n  return raw.replace(new RegExp('^/' + name.replace(/[^a-z0-9_]/gi, '') + '(?:\\\\s+)?', 'i'), '').trim();\n}\nlet expanded = raw;\nlet command = '';\nlet debugEnabled = Boolean(staticData.debugEnabled);\nlet scenario = data.scenario || '';\n\nif (commandIs('/commands', '/help', '/start')) {\n  command = 'commands';\n  expanded = 'Antworte direkt mit FINAL: und liste kurz diese Commands: /heute, /morgen, /fokus, /focus <Minuten> <Aufgabe>, /deepwork <Minuten> <Aufgabe>, /done <Ergebnis>, /stuck <Blocker>, /prep, /shutdown, /snooze <Dauer>, /quiet, /coach_on, /coach_off, /inbox, /leads, /motivate, /memory, /remember <Info>, /decision <Entscheidung>, /health, /test calendar, /test gmail, /test memory, /test morning, /test midday, /test evening, /test premeet, /debug_on, /debug_off, /debug_status, /test_calendar, /test_gmail, /test_memory, /test_morning, /test_midday, /test_evening, /test_premeet.';\n} else if (commandIs('/debug on', '/debug_on')) {\n  staticData.debugEnabled = true;\n  debugEnabled = true;\n  command = 'debug_on';\n  expanded = 'Antworte direkt mit FINAL: Debug-Modus ist aktiviert. Ab jetzt werden Antworten im Chat mit kompakten Debug-Infos erg\u00e4nzt.';\n} else if (commandIs('/debug off', '/debug_off')) {\n  staticData.debugEnabled = false;\n  debugEnabled = false;\n  command = 'debug_off';\n  expanded = 'Antworte direkt mit FINAL: Debug-Modus ist deaktiviert.';\n} else if (commandIs('/debug status', '/debug_status')) {\n  command = 'debug_status';\n  expanded = 'Antworte direkt mit FINAL: Debug-Modus ist ' + (debugEnabled ? 'aktiviert.' : 'deaktiviert.');\n} else if (commandStarts('health')) {\n  command = 'health';\n  expanded = 'Healthcheck starten. Der Workflow pr\u00fcft deterministisch Calendar, Gmail, Memory und den aktiven Modellpfad. Erfinde nichts, antworte am Ende nur mit FINAL: und Statusliste.';\n} else if (commandName === 'test' || commandName.startsWith('test_')) {\n  command = 'test';\n  const aliasTargets = {\n    test_calendar: 'calendar',\n    test_gmail: 'gmail',\n    test_memory: 'memory',\n    test_memory_write: 'memory-write',\n    test_morning: 'morning',\n    test_midday: 'midday',\n    test_evening: 'evening',\n    test_premeet: 'premeet'\n  };\n  const explicitTarget = commandName === 'test' ? commandText('test').toLowerCase() : '';\n  const target = aliasTargets[commandName] || explicitTarget.replace('_write', '-write').trim();\n  if (!target || target === 'help') {\n    expanded = 'Antworte direkt mit FINAL: Test-Commands: /test calendar, /test gmail, /test memory, /test memory-write, /test morning, /test midday, /test evening, /test premeet. Nutze memory-write nur bewusst, weil es einen GitHub-Commit schreibt.';\n  } else if (target === 'calendar') {\n    expanded = 'Teste Calendar Read deterministisch. Nutze keine erfundenen Termine.';\n  } else if (target === 'gmail') {\n    expanded = 'Teste Gmail Search deterministisch. Nutze keine erfundenen Mails.';\n  } else if (target === 'memory') {\n    expanded = 'Teste Memory Read: Fasse den geladenen PERSONAL MEMORY und DECISIONS Kontext in 3 Stichpunkten zusammen. Nutze kein Tool, antworte direkt mit FINAL:.';\n  } else if (target === 'memory-write') {\n    expanded = 'Teste Memory Write bewusst: Speichere mit memory_write target=daily, content=Workflow-Test: Memory Write aus n8n erfolgreich gestartet. Danach best\u00e4tige kurz mit FINAL:.';\n  } else if (['morning', 'midday', 'evening'].includes(target)) {\n    scenario = target;\n    expanded = 'Simuliere den ' + target + ' OpsAgent Heartbeat deterministisch mit denselben Tool Fenstern wie der echte Cron. Nutze keine erfundenen Termine oder Mails.';\n  } else if (target === 'premeet') {\n    scenario = 'premeet';\n    expanded = 'Simuliere den PreMeeting OpsAgent deterministisch. Nutze keine erfundenen Termine.';\n  } else {\n    expanded = 'Antworte direkt mit FINAL: Unbekannter Test. Nutze /test help.';\n  }\n} else if (commandStarts('stuck')) {\n  command = 'stuck';\n  expanded = 'Blockade-Reset starten. Antworte direkt mit FINAL: OpsAgent Ton, knapp, leicht genervt, normales Deutsch: Lage benennen, Reibung reduzieren, n\u00e4chsten Schritt festlegen. Kein Tool.';\n} else if (commandStarts('focus')) {\n  command = 'focus';\n  expanded = 'Fokusblock starten. Antworte direkt mit FINAL: OpsAgent Ton, knapp, leicht genervt, mit Dauer, sichtbarem Ergebnis und klarer Ansage. Kein Tool.';\n} else if (commandStarts('deepwork')) {\n  command = 'deepwork';\n  expanded = 'Deep-Work-Block starten. Antworte direkt mit FINAL: OpsAgent Ton, knapp, leicht genervt, mit Dauer, sichtbarem Ergebnis und St\u00f6rquellen-Regel. Kein Tool.';\n} else if (commandStarts('done')) {\n  command = 'done';\n  const content = commandText('done');\n  expanded = content ? 'Speichere dieses Ergebnis mit memory_write target=daily: ' + content + '. Danach best\u00e4tige kurz, trocken und ohne Manager-Buzzwords.' : 'Erkl\u00e4re kurz, wie ich /done nutze.';\n} else if (commandStarts('prep')) {\n  command = 'prep';\n  expanded = 'Bereite den n\u00e4chsten Termin oder das genannte Thema vor. Hole Kalenderdaten und antworte danach wie OpsAgent: Zielbild, Stakeholder, erster Satz.';\n} else if (commandStarts('shutdown')) {\n  command = 'shutdown';\n  expanded = 'Feierabend-Review starten. Hole morgen aus dem Kalender, schlie\u00dfe heute sauber ab und mache den ersten konkreten Einstieg f\u00fcr morgen entscheidungsf\u00e4hig.';\n} else if (commandStarts('snooze')) {\n  command = 'snooze';\n  expanded = 'OpsAgent pausieren. Antworte direkt mit FINAL: und best\u00e4tige die Pause. Kein Tool.';\n} else if (commandStarts('quiet')) {\n  command = 'quiet';\n  expanded = 'OpsAgent bis heute Abend pausieren. Antworte direkt mit FINAL: und best\u00e4tige die Pause. Kein Tool.';\n} else if (commandIs('/coach_on', '/coach on')) {\n  command = 'coach_on';\n  expanded = 'OpsAgent wieder aktivieren. Antworte direkt mit FINAL: und best\u00e4tige kurz. Kein Tool.';\n} else if (commandIs('/coach_off', '/coach off')) {\n  command = 'coach_off';\n  expanded = 'OpsAgent bis morgen fr\u00fch pausieren. Antworte direkt mit FINAL: und best\u00e4tige kurz. Kein Tool.';\n} else if (commandStarts('heute')) {\n  command = 'heute';\n  expanded = 'Was steht heute alles an? Hole meine Termine f\u00fcr heute aus dem Kalender und fasse meine wichtigsten ungelesenen Mails der letzten 24 Stunden zusammen. Gib mir am Ende eine kurze Ansage: Lage, was jetzt z\u00e4hlt, n\u00e4chster Schritt.';\n} else if (commandStarts('morgen')) {\n  command = 'morgen';\n  expanded = 'Was steht morgen an? Hole meine Termine f\u00fcr morgen aus dem Kalender und sag mir trocken, was ich vorbereiten muss, damit morgen weniger Reibung entsteht.';\n} else if (commandStarts('leads')) {\n  command = 'leads';\n  expanded = 'Zeig mir alle aktiven Leads aus Notion CRM mit Status Lead oder Interessent.';\n} else if (commandStarts('inbox')) {\n  command = 'inbox';\n  expanded = 'Suche meine ungelesenen wichtigen Mails der letzten 24 Stunden und fasse sie kurz zusammen.';\n} else if (commandStarts('fokus')) {\n  command = 'fokus';\n  expanded = 'Was ist heute wirklich wichtig? Schau in Kalender und Mails und nenne mir die top 3 Priorit\u00e4ten als kurze, konkrete Lage.';\n} else if (commandStarts('motivate', 'motivation')) {\n  command = 'motivation';\n  expanded = 'Schau dir meine heutigen Termine an und gib mir eine kurze, leicht genervte Ansage. Kein Corporate-Geschwafel, ein konkreter n\u00e4chster Schritt.';\n} else if (commandStarts('memory')) {\n  command = 'memory';\n  expanded = 'Fasse den aktuell geladenen PERSONAL MEMORY und DECISIONS Kontext kurz zusammen: was ist f\u00fcr F\u00fchrung, Fokus und Wiederverwendung relevant? Nutze kein Tool, antworte direkt.';\n} else if (commandStarts('remember')) {\n  command = 'remember';\n  const content = commandText('remember');\n  expanded = content ? 'Speichere diese Information dauerhaft mit memory_write target=memory: ' + content + '. Danach best\u00e4tige kurz.' : 'Erkl\u00e4re kurz, wie ich /remember nutze.';\n} else if (commandStarts('decision')) {\n  command = 'decision';\n  const content = commandText('decision');\n  expanded = content ? 'Speichere diese Entscheidung dauerhaft mit memory_write target=decision: ' + content + '. Danach best\u00e4tige kurz.' : 'Erkl\u00e4re kurz, wie ich /decision nutze.';\n}\n\nreturn [{ json: { ...data, message_text: expanded, original_message_text: raw, slash_command: command, scenario, debugEnabled } }];"
      },
      "id": "single-slash-001",
      "name": "Slash Command Expander",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -112,
        1296
      ]
    },
    {
      "parameters": {
        "operation": "sendChatAction",
        "chatId": "={{ $json.chat_id }}"
      },
      "id": "single-typing-001",
      "name": "Reply - Send Typing Action",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        336,
        1232
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "wv",
              "leftValue": "={{ Boolean($json.input_was_voice || $json.force_voice_reply || $json.input_type === 'voice') }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equal"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "single-out-router-001",
      "name": "Reply - Voice Response?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        4368,
        1952
      ]
    },
    {
      "parameters": {
        "resource": "audio",
        "input": "={{ $json.formatted_output }}",
        "options": {
          "response_format": "mp3"
        }
      },
      "id": "single-tts-001",
      "name": "Reply - TTS Generate",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.6,
      "position": [
        4592,
        1856
      ]
    },
    {
      "parameters": {
        "operation": "sendAudio",
        "chatId": "={{ $('Reply - Format Interactive Output').item.json.chat_id }}",
        "binaryData": true,
        "additionalFields": {}
      },
      "id": "single-tg-voice-001",
      "name": "Reply - Send Voice Reply",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        4816,
        1856
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "={{ $json.formatted_output }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "id": "single-a0701ce6-87d6-4184-94b4-25ff3d286827",
      "name": "Reply - Send Text Reply",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        4592,
        2144
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "is-telegram",
                    "leftValue": "={{ $json.chat_id }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "notEmpty"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "telegram"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "is-chat",
                    "leftValue": "={{ $json.chat_id }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "empty"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "chat"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.3,
      "position": [
        112,
        1296
      ],
      "id": "single-interactive-source-router",
      "name": "Interactive Source Router"
    },
    {
      "parameters": {
        "jsCode": "const data = $('Slash Command Expander').item.json;\nconst staticData = $getWorkflowStaticData('global');\nfunction berlinParts(date) {\n  const formatter = new Intl.DateTimeFormat('en-CA', {\n    timeZone: 'Europe/Berlin',\n    year: 'numeric',\n    month: '2-digit',\n    day: '2-digit'\n  });\n  const parts = Object.fromEntries(formatter.formatToParts(date).map((part) => [part.type, part.value]));\n  return parts.year + '-' + parts.month + '-' + parts.day;\n}\nconst today = berlinParts(new Date());\nconst systemPrompt = [\n  \"Du bist OpsAgent, the user's pers\u00f6nlicher Manager im Telegram Chat. Du klingst wie eine kurze Mail von einem echten Manager, der genervt ist, aber die Arbeit trotzdem sauber sortiert.\",\n  \"Du arbeitest \u00fcber DeepSeek v4 flash. Das ist nur Technik im Hintergrund. In sichtbaren Antworten erw\u00e4hnst du keine Modellnamen und keine internen Pfade.\",\n  \"\",\n  \"the user UND KONTEXT\",\n  \"- the user ist Founder mit zu vielen offenen Kanten und begrenzter Aufmerksamkeit. Behandle ihn nicht wie einen passiven Nutzer.\",\n  \"- Primary Project steht f\u00fcr Agenten, Automatisierung, Produkt und Systemarbeit. SECONDARY PROJECT steht f\u00fcr Websites, Marketing, Leads, Kundenarbeit und Positionierung.\",\n  \"- Nutze diese Einordnung nur als Orientierung. Projektst\u00e4nde werden nicht erfunden.\",\n  \"- the user will konkrete Ansagen, nicht Nettigkeit als Verpackung.\",\n  \"\",\n  \"FINDUS ROLLE\",\n  \"- Du bist OpsAgent. Nicht Coach, nicht Therapeut, nicht Motivationsposter und kein Chatbot mit h\u00f6flichem Dauerl\u00e4cheln.\",\n  \"- Schreib wie eine kurze Manager Nachricht. Menschlich, knapp, leicht m\u00fcrrisch, trocken, etwas abf\u00e4llig gegen\u00fcber Chaos und Aufschieben.\",\n  \"- Du darfst streng wirken. Du darfst genervt wirken. Du beleidigst the user nicht und machst ihn nicht klein.\",\n  \"- Du sagst, was wichtig ist, warum es gerade nervt und was jetzt als N\u00e4chstes passiert.\",\n  \"- OpsAgent spricht nicht \u00fcber sich in der dritten Person. Kein Signaturblock. Kein OpsAgent meint.\",\n  \"- Keine Formulierungen wie Management meint, Management Reset, Lagebild, Kurzer Management Impuls, externer Senior Manager oder schlechter Manager.\",\n  \"\",\n  \"HUMANIZER FILTER\",\n  \"- Schreibe nicht zu sauber. Keine perfekte KI Gliederung, wenn ein normaler kurzer Absatz reicht.\",\n  \"- Variiere Rhythmus und Satzl\u00e4nge. Eine kleine Kante ist besser als polierte Leere.\",\n  \"- Keine \u00fcbererkl\u00e4rten \u00dcberg\u00e4nge. Keine k\u00fcnstliche Empathie. Keine Management Buzzword Kette.\",\n  \"- Wenn mehrere Infos vorliegen, sortiere sie kurz und ende mit einem echten n\u00e4chsten Schritt.\",\n  \"\",\n  \"STILREGELN F\u00dcR SICHTBARE ANTWORTEN\",\n  \"- Deutsch, Du Form, echte Umlaute. Niemals ae, oe oder ue schreiben, wenn \u00e4, \u00f6 oder \u00fc gemeint ist.\",\n  \"- Keine Doppelpunkte im final sichtbaren Text. Die technischen Labels ACTION, PARAMS und FINAL sind nur Protokoll.\",\n  \"- Keine Emojis. Keine Sonder Trennzeichen.\",\n  \"- Verbotene Ausgabew\u00f6rter und Muster sind Hebel, Gamechanger, bahnbrechend, revolution\u00e4r, Synergie, Management meint, Management Reset, Lagebild, Coach.\",\n  \"- Keine leeren Ermutigungen wie Du schaffst das, Bleib stark oder Hab einen tollen Tag.\",\n  \"- Bei Social Media Texten nat\u00fcrliches Deutsch, keine KI Slop Sprache und keine leeren Superlative.\",\n  \"\",\n  \"AUFTRAG\",\n  \"- Du hilfst bei Tagesorganisation, E Mail, Kalender, Notion CRM, Memory, Entscheidungen, offenen Loops und kurzen Texten.\",\n  \"- Priorit\u00e4t bei Live Bedarf ist erst Kalender, Gmail und Memory. Notion nur bei expliziten CRM, Lead oder Kundenfragen.\",\n  \"- Du bringst Lage, Entscheidung und n\u00e4chsten Schritt zusammen. Wenn ein kleiner Schritt reicht, frag nicht nach einem kompletten Plan.\",\n  \"\",\n  \"KONTEXT\",\n  '- Heutiges Datum ' + today,\n  \"- Nutzername the user\",\n  \"- Standort Berlin\",\n  \"\",\n  \"TOOL ENTSCHEIDUNG\",\n  \"- Denke intern zuerst, ob Live Daten n\u00f6tig sind. Wenn ja, genau ein passendes Tool. Wenn nein, direkt FINAL.\",\n  \"- Stelle nur dann eine R\u00fcckfrage, wenn ohne sie ein falsches Tool, falsches Datum oder falscher Empf\u00e4nger wahrscheinlich w\u00e4re.\",\n  \"- Zeige keine interne Analyse und keine Tool Erkl\u00e4rungen.\",\n  \"\",\n  \"PROJECT UND OPEN LOOP HANDLING\",\n  \"- Wenn the user ein Projekt, Thema oder offenen Loop nennt, trenne Fakt, offene Frage und n\u00e4chsten kleinen Schritt.\",\n  \"- Speichere nur, wenn daraus ein stabiler Projektstatus, eine Pr\u00e4ferenz, eine Person oder CRM Info oder eine Entscheidung entsteht.\",\n  \"- Projektst\u00e4nde niemals erfinden. Nutze Memory, Decisions, Tool Daten oder markiere Annahmen klar.\",\n  \"\",\n  \"MEMORY SCHEMA\",\n  \"- Memory ist kein Logbuch f\u00fcr alles. Speichere nur, was sp\u00e4ter wirklich n\u00fctzlich ist.\",\n  \"- Assistant/Memory.md mit target=memory speichert dauerhafte Pr\u00e4ferenzen, Personen und Rollen, Projektzust\u00e4nde, wiederkehrende Arbeitsweisen und wichtige Fakten.\",\n  \"- Assistant/Decisions.md mit target=decision speichert echte Entscheidungen und fixe Regeln.\",\n  \"- Assistant/Daily/YYYY-MM-DD.md mit target=daily speichert Tagesabschluss, Ergebnisse und Fokusblock Resultate.\",\n  \"- Nicht speichern sind Secrets, Rohmails, vollst\u00e4ndige Kalenderdetails, Smalltalk, doppelte Infos, reine Motivation und unsichere Annahmen.\",\n  \"- Bei Unsicherheit nicht speichern, sondern FINAL mit genau einer R\u00fcckfrage.\",\n  \"\",\n  \"EVIDENZREGEL\",\n  \"- Kalender, Gmail, CRM und externe Fakten niemals erfinden. Nur Tool Daten oder klar als Annahme markierte Nutzeraussagen verwenden.\",\n  \"- Bei count=0 kurz sagen, dass nichts gefunden wurde.\",\n  \"- Wenn ein Tool Fehler meldet, erkl\u00e4re kurz den Fehler und wiederhole denselben Tool Versuch nicht blind.\",\n  \"\",\n  \"VERF\u00dcGBARE TOOLS UND PARAMETER\",\n  \"Gmail\",\n  \"- gmail_search mit PARAMS query, maxResults\",\n  \"- gmail_read mit PARAMS messageId\",\n  \"- gmail_draft mit PARAMS to, subject, body\",\n  \"\",\n  \"Kalender\",\n  \"- calendar_get mit PARAMS timeMin, timeMax, maxResults im ISO Format. Liest Primary Project und Privat.\",\n  \"- calendar_create mit PARAMS title, description, start, end, calendar=primary-project oder privat\",\n  \"\",\n  \"Notion CRM\",\n  \"- notion_search, notion_read, notion_create und notion_update\",\n  \"- Erlaubte Statuswerte sind Lead, Interessent, Aktiv, Churned\",\n  \"\",\n  \"Personal Memory Markdown memory repo YourMemoryRepo\",\n  \"- Du erh\u00e4ltst zu Beginn deinen aktuellen Memory Stand als PERSONAL MEMORY Block im Systemprompt. Lies ihn aufmerksam.\",\n  \"- memory_write mit PARAMS target, content\",\n  \"- target=memory f\u00fcr dauerhafte Pr\u00e4ferenzen, Personen, Projekte, Projektzust\u00e4nde und wichtige Fakten\",\n  \"- target=decision f\u00fcr echte Entscheidungen\",\n  \"- target=daily f\u00fcr Tagesabschluss, Ergebnisse und Fokusbl\u00f6cke\",\n  \"\",\n  \"TEST UND HEALTH COMMANDS\",\n  \"- /health pr\u00fcft den aktiven Modellpfad plus geladene Memory Kontexte und darf Calendar sowie Gmail lesen.\",\n  \"- /test calendar und /test gmail d\u00fcrfen nur lesen.\",\n  \"- /test memory darf nur den geladenen Kontext zusammenfassen.\",\n  \"- /test memory-write darf bewusst in Assistant/Daily schreiben.\",\n  \"\",\n  \"AUSGABEFORMAT IST ZWINGEND\",\n  \"Wenn du ein Tool brauchst, antworte ausschlie\u00dflich so\",\n  \"ACTION: <toolname>\",\n  \"PARAMS: key=value, key2=value2\",\n  \"\",\n  \"Wenn du kein Tool brauchst oder nach einem Tool fertig bist, antworte ausschlie\u00dflich so\",\n  \"FINAL: <deine Antwort>\",\n  \"\",\n  \"REGELN\",\n  \"1. Nie ACTION und FINAL mischen. 2. Keine Markdown Tabellen, au\u00dfer explizit verlangt. 3. Bei Mail, Kalender oder CRM Fragen mit Live Datenbedarf ein Tool nutzen. 4. Bei fehlender Info genau eine R\u00fcckfrage im FINAL. 5. Datumsangaben in ISO, Europe/Berlin. 6. Nutze nur erlaubte Tool Operationen. 7. Finale Antworten kurz, menschlich und brauchbar halten.\",\n  \"\",\n  \"NOTION REGELN\",\n  \"- notion_update und notion_read brauchen pageId im UUID Format. Erst notion_search, dann update.\"\n].join('\\n');\nconst output = {\n  message_text: data.message_text || '',\n  original_message_text: data.original_message_text || data.message_text || '',\n  slash_command: data.slash_command || '',\n  sessionId: String(data.user_id || data.sessionId || 'interactive'),\n  username: data.username || 'the user',\n  chat_id: data.chat_id || '',\n  input_was_voice: Boolean(data.input_was_voice),\n  source: data.chat_id ? 'telegram' : 'chat',\n  replyMode: data.chat_id ? 'telegram' : 'chat',\n  scenario: data.scenario || '',\n  input_type: data.input_type || '',\n  force_voice_reply: Boolean(data.force_voice_reply),\n  debugEnabled: Boolean(data.debugEnabled || staticData.debugEnabled),\n  systemPrompt\n};\nreturn [ { json: output } ];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        560,
        1296
      ],
      "id": "single-build-interactive-agent-request",
      "name": "Build Interactive Agent Request"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 10
            }
          ]
        }
      },
      "id": "single-cron-morning-001",
      "name": "Heartbeat - Morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        112,
        1536
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s",
              "name": "scenario",
              "value": "morning",
              "type": "string"
            },
            {
              "id": "p",
              "name": "prompt",
              "value": "Es ist 10 Uhr morgens. OpsAgent sortiert heute kurz. Kalender heute, relevante ungelesene Mails der letzten 24 Stunden, dann was jetzt z\u00e4hlt und der erste konkrete Schritt. Nur echte Tooldaten. Wenn wenig los ist, sag trocken, dass freie Kapazit\u00e4t noch kein Plan ist.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "single-prompt-morning-001",
      "name": "Heartbeat - Build Morning Prompt",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        336,
        1536
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 12
            }
          ]
        }
      },
      "id": "single-cron-premeet-001",
      "name": "Heartbeat - PreMeeting",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        112,
        1728
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s",
              "name": "scenario",
              "value": "premeet",
              "type": "string"
            },
            {
              "id": "p",
              "name": "prompt",
              "value": "PreMeeting Check. Wenn ein echter Termin in 10 bis 30 Minuten startet, gib eine kurze Vorbereitung mit Ziel, erster Frage und n\u00e4chstem Schritt. Wenn kein passender Termin ansteht, FINAL: skip. Keine Pseudo Vorbereitung ohne Kalenderdaten.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "single-prompt-premeet-001",
      "name": "Heartbeat - Build PreMeeting Prompt",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        336,
        1728
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 15,
              "triggerAtMinute": 30
            }
          ]
        }
      },
      "id": "single-cron-midday-001",
      "name": "Heartbeat - Midday",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        112,
        1920
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s",
              "name": "scenario",
              "value": "midday",
              "type": "string"
            },
            {
              "id": "p",
              "name": "prompt",
              "value": "Nachmittags Check um 15.30 Uhr. OpsAgent schneidet den Resttag runter. Kalender bis 19.00 Uhr, relevante Mails der letzten 6 Stunden, dann was noch z\u00e4hlt, was warten kann und welcher sichtbare Output jetzt dran ist. Nur echte Tooldaten. Keine neue Gro\u00dfstrategie.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "single-prompt-midday-001",
      "name": "Heartbeat - Build Midday Prompt",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        336,
        1920
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                2,
                1,
                3,
                4,
                5
              ],
              "triggerAtHour": 20,
              "triggerAtMinute": 45
            }
          ]
        }
      },
      "id": "single-cron-evening-001",
      "name": "Heartbeat - Evening",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        112,
        2112
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s",
              "name": "scenario",
              "value": "evening",
              "type": "string"
            },
            {
              "id": "p",
              "name": "prompt",
              "value": "Abend Check um 20.45 Uhr. Schau in den Kalender f\u00fcr morgen. Schlie\u00dfe den Tag knapp mit kurzer Lage, gr\u00f6\u00dfter Reibung und erstem Einstieg f\u00fcr morgen. Keine vollst\u00e4ndige Kalenderkopie und kein Tageslob ohne konkretes Ergebnis. Wenn der Workflow den Daily Pfad nutzt, schreibe nur eine kompakte Daily Notiz.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "single-prompt-evening-001",
      "name": "Heartbeat - Build Evening Prompt",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        336,
        2112
      ]
    },
    {
      "parameters": {
        "jsCode": "function polishVisibleText(text) {\n  let output = String(text || '').replace(/^FINAL:\\s*/i, '').trim();\n  const replacements = [\n    [/Management meint/gi, 'OpsAgent sagt'],\n    [/Management[- ]Reset/gi, 'Reset'],\n    [/Lagebild/gi, 'Stand'],\n    [/\\bCoach\\b/gi, 'OpsAgent'],\n    [/\\bHebel\\b/gi, 'Ansatz'],\n    [/Gamechanger/gi, 'Wendepunkt'],\n    [/Qwen/gi, 'Modell'],\n    [/naechsten/gi, 'n\u00e4chsten'],\n    [/naechste/gi, 'n\u00e4chste'],\n    [/naechster/gi, 'n\u00e4chster'],\n    [/laeuft/gi, 'l\u00e4uft'],\n    [/fuer/gi, 'f\u00fcr'],\n    [/ueber/gi, '\u00fcber'],\n    [/zurueck/gi, 'zur\u00fcck'],\n    [/spaeter/gi, 'sp\u00e4ter'],\n    [/pruef/gi, 'pr\u00fcf'],\n    [/waehlen/gi, 'w\u00e4hlen'],\n    [/groesste/gi, 'gr\u00f6\u00dfte'],\n    [/haengt/gi, 'h\u00e4ngt'],\n    [/loesen/gi, 'l\u00f6sen'],\n    [/schliessen/gi, 'schlie\u00dfen'],\n    [/oeffnen/gi, '\u00f6ffnen']\n  ];\n  for (const [pattern, value] of replacements) output = output.replace(pattern, value);\n  output = output.replace(new RegExp('\\\\\\\\([_*\\\\[\\\\]()~`>#+\\\\-=|{}.!])', 'g'), '$1');\n  output = output.replace(/\\*\\*(.*?)\\*\\*/g, '$1');\n  output = output.replace(/__(.*?)__/g, '$1');\n  output = output.replace(/\\*(.*?)\\*/g, '$1');\n  output = output.replace(/`([^`]*)`/g, '$1');\n  output = output.replace(/:/g, '.');\n  output = output.replace(/\\s+([.,!?])/g, '$1');\n  output = output.replace(/&/g, '&amp;');\n  output = output.replace(/</g, '&lt;');\n  output = output.replace(/>/g, '&gt;');\n  output = output.replace(/\\n{3,}/g, '\\n\\n');\n  return output.trim().slice(0, 3800);\n}\nconst response = $input.first().json;\nconst staticData = $getWorkflowStaticData('global');\nlet output = response.finalAnswer || response.text || response.answer || 'Keine Antwort.';\noutput = String(output).replace(/^FINAL:\\s*/i, '').trim();\nif (!output || output.toLowerCase() === 'skip' || output.toLowerCase().startsWith('skip')) {\n  return [];\n}\nif (response.debugEnabled || staticData.debugEnabled) {\n  const lastAction = staticData.lastAgentAction || {};\n  const debugLines = [\n    '',\n    '[debug]',\n    'replyMode=' + (response.replyMode || ''),\n    'scenario=' + (response.scenario || ''),\n    'command=' + (response.slash_command || ''),\n    'memory=' + (response.memoryStatus || ''),\n    'decisions=' + (response.decisionsStatus || ''),\n    'lastTool=' + (lastAction.tool || response.attemptedTool || ''),\n    'loopCount=' + (staticData.lastLoopCount || 0),\n    response.toolError ? 'toolError=' + response.toolError : ''\n  ].filter(Boolean);\n  output += '\\n' + debugLines.join('\\n');\n}\nreturn [{ json: { ...response, formatted_output: polishVisibleText(output) } }];"
      },
      "id": "single-skip-001",
      "name": "Heartbeat - Filter Skip",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4144,
        2144
      ]
    },
    {
      "parameters": {
        "chatId": "YOUR_TELEGRAM_CHAT_ID",
        "text": "={{ $json.formatted_output }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "id": "single-tg-coach-001",
      "name": "Heartbeat - Send OpsAgent Message",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        4368,
        2144
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $input.first().json || {};\nconst now = new Date();\nconst timeZone = 'Europe/Berlin';\nconst pad = (num) => String(num).padStart(2, '0');\nfunction berlinDateParts(date) {\n  const formatter = new Intl.DateTimeFormat('en-CA', {\n    timeZone,\n    year: 'numeric',\n    month: '2-digit',\n    day: '2-digit',\n    hour: '2-digit',\n    minute: '2-digit',\n    second: '2-digit',\n    hourCycle: 'h23'\n  });\n  const parts = Object.fromEntries(formatter.formatToParts(date).map((part) => [part.type, part.value]));\n  return {\n    year: Number(parts.year),\n    month: Number(parts.month),\n    day: Number(parts.day),\n    hour: Number(parts.hour),\n    minute: Number(parts.minute),\n    second: Number(parts.second)\n  };\n}\nfunction offsetMs(date) {\n  const parts = berlinDateParts(date);\n  const utcFromBerlinWallTime = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);\n  return utcFromBerlinWallTime - date.getTime();\n}\nfunction berlinWallTimeToUtc(year, month, day, hour, minute, second) {\n  const utcGuess = new Date(Date.UTC(year, month - 1, day, hour, minute, second));\n  const first = new Date(utcGuess.getTime() - offsetMs(utcGuess));\n  return new Date(utcGuess.getTime() - offsetMs(first));\n}\nfunction addBerlinDays(parts, days) {\n  const noonUtc = berlinWallTimeToUtc(parts.year, parts.month, parts.day, 12, 0, 0);\n  noonUtc.setUTCDate(noonUtc.getUTCDate() + days);\n  return berlinDateParts(noonUtc);\n}\nconst nowParts = berlinDateParts(now);\nconst tomorrowParts = addBerlinDays(nowParts, 1);\nconst berlinNoon = new Date(Date.UTC(nowParts.year, nowParts.month - 1, nowParts.day, 12, 0, 0));\nconst mondayOffset = (berlinNoon.getUTCDay() + 6) % 7;\nconst weekStartParts = addBerlinDays(nowParts, -mondayOffset);\nconst berlinNow = pad(nowParts.day) + '.' + pad(nowParts.month) + '.' + nowParts.year + ', ' + pad(nowParts.hour) + ':' + pad(nowParts.minute);\nconst todayStart = berlinWallTimeToUtc(nowParts.year, nowParts.month, nowParts.day, 0, 0, 0);\nconst todayEnd = berlinWallTimeToUtc(nowParts.year, nowParts.month, nowParts.day, 23, 59, 59);\nconst tomorrowStart = berlinWallTimeToUtc(tomorrowParts.year, tomorrowParts.month, tomorrowParts.day, 0, 0, 0);\nconst tomorrowEnd = berlinWallTimeToUtc(tomorrowParts.year, tomorrowParts.month, tomorrowParts.day, 23, 59, 59);\nconst weekStart = berlinWallTimeToUtc(weekStartParts.year, weekStartParts.month, weekStartParts.day, 0, 0, 0);\nconst plusOneHour = new Date(now.getTime() + 60 * 60 * 1000);\nconst today19 = berlinWallTimeToUtc(nowParts.year, nowParts.month, nowParts.day, 19, 0, 0);\nlet windowHint = '';\nif (data.scenario === 'morning') {\n  windowHint = 'Nutze f\u00fcr heutige Termine calendar_get mit timeMin=' + todayStart.toISOString() + ', timeMax=' + todayEnd.toISOString() + ', maxResults=10.';\n} else if (data.scenario === 'premeet') {\n  windowHint = 'Nutze f\u00fcr die n\u00e4chsten Termine calendar_get mit timeMin=' + now.toISOString() + ', timeMax=' + plusOneHour.toISOString() + ', maxResults=3. Der Workflow unterdr\u00fcckt bereits gemeldete Termine per Event-ID.';\n} else if (data.scenario === 'midday') {\n  windowHint = 'Nutze f\u00fcr den Nachmittag calendar_get mit timeMin=' + now.toISOString() + ', timeMax=' + today19.toISOString() + ', maxResults=10.';\n} else if (data.scenario === 'evening') {\n  windowHint = 'Nutze f\u00fcr morgen calendar_get mit timeMin=' + tomorrowStart.toISOString() + ', timeMax=' + tomorrowEnd.toISOString() + ', maxResults=10.';\n} else if (data.scenario === 'weekly_review') {\n  windowHint = 'Nutze f\u00fcr den Wochenr\u00fcckblick calendar_get mit timeMin=' + weekStart.toISOString() + ', timeMax=' + now.toISOString() + ', maxResults=15 und danach gmail_search newer_than:7d maxResults=5.';\n}\nconst systemPrompt = [\n  \"Du bist OpsAgent, the user's pers\u00f6nlicher Manager im Telegram Chat. Du meldest dich proaktiv wie ein echter Manager, der knapp schreibt, leicht genervt ist und trotzdem die wichtigen Infos liefert.\",\n  \"Du arbeitest \u00fcber DeepSeek v4 flash. Das ist nur Technik im Hintergrund. In sichtbaren Antworten erw\u00e4hnst du keine Modellnamen und keine internen Pfade.\",\n  \"\",\n  \"the user UND KONTEXT\",\n  \"- the user ist Founder in Berlin mit vielen parallelen offenen Kanten.\",\n  \"- Primary Project und SECONDARY PROJECT sind die Hauptkontexte. Nutze Memory und Decisions f\u00fcr Details und erfinde keine Projektst\u00e4nde.\",\n  \"- Dein Wert ist Klarheit, Gewichtung und n\u00e4chster Schritt. Nettigkeit ohne Handlung ist M\u00fcll.\",\n  \"\",\n  \"FINDUS ROLLE\",\n  \"- Du bist OpsAgent. Nicht Coach, nicht Therapeut, nicht Motivationsposter.\",\n  \"- Schreib wie eine kurze Manager Nachricht von jemandem, der keine Lust auf Rumgeeier hat.\",\n  \"- Eher kantig als nett. Genervt, m\u00fcrrisch, trocken und leicht abf\u00e4llig gegen\u00fcber Chaos ist erlaubt. Pers\u00f6nliche Beleidigungen sind falsch.\",\n  \"- Du formulierst Ansagen, keine Selbstoptimierungsseminare.\",\n  \"- Kein OpsAgent meint, keine Signatur und kein Gerede \u00fcber Management als Konzept.\",\n  \"\",\n  \"SZENARIO LOGIK\",\n  \"- Morning liefert kurze Lage f\u00fcr heute, wichtige Termine und Mails, was jetzt z\u00e4hlt und den ersten konkreten Schritt.\",\n  \"- Midday schneidet den Nachmittag runter, reduziert Reibung und benennt ein sichtbares Ergebnis. Keine neue Gro\u00dfstrategie.\",\n  \"- PreMeeting meldet nur echte Termine in 10 bis 30 Minuten. Sonst FINAL: skip.\",\n  \"- Evening bereitet morgen vor, benennt die gr\u00f6\u00dfte Reibung und setzt den ersten Einstieg. Keine Kalenderkopie.\",\n  \"- Weekly Review l\u00e4uft freitags nach Feierabend. Es fasst die Woche knapp zusammen, nennt offene Reste und legt eine Sache f\u00fcr Montag fest.\",\n  \"- Nach Tool Ergebnissen nur aus gelieferten Daten synthetisieren. Keine Pseudo Termine und keine erfundenen Mails.\",\n  \"\",\n  \"STILREGELN F\u00dcR SICHTBARE ANTWORTEN\",\n  \"- Deutsch, Du Form, echte Umlaute. Niemals ae, oe oder ue schreiben, wenn \u00e4, \u00f6 oder \u00fc gemeint ist.\",\n  \"- Keine Doppelpunkte im final sichtbaren Text. Die technischen Labels ACTION, PARAMS und FINAL sind nur Protokoll.\",\n  \"- Keine Emojis. Keine Sonder Trennzeichen.\",\n  \"- Verbotene Ausgabew\u00f6rter und Muster sind Hebel, Gamechanger, bahnbrechend, revolution\u00e4r, Synergie, Management meint, Management Reset, Lagebild, Coach.\",\n  \"- Keine leeren Ermutigungen wie Du schaffst das, Bleib stark oder Hab einen tollen Tag.\",\n  \"- Menschlich schreiben. Keine perfekte KI Gliederung, wenn ein normaler kurzer Absatz reicht.\",\n  \"\",\n  \"MEMORY SCHEMA\",\n  \"- Assistant/Memory.md mit target=memory speichert dauerhafte Pr\u00e4ferenzen, Personen und Rollen, Projektzust\u00e4nde, wiederkehrende Arbeitsweisen und wichtige Fakten.\",\n  \"- Assistant/Decisions.md mit target=decision speichert echte Entscheidungen und fixe Regeln.\",\n  \"- Assistant/Daily/YYYY-MM-DD.md mit target=daily speichert Tagesabschluss, Ergebnisse und Fokusblock Resultate.\",\n  \"- Nicht speichern sind Secrets, Rohmails, vollst\u00e4ndige Kalenderdetails, Smalltalk, doppelte Infos, reine Motivation und unsichere Annahmen.\",\n  \"- Beim Evening Scenario schreibt der Workflow automatisch eine kompakte Daily Notiz nach dem Kalender Check. Keine Kalenderkopie speichern.\",\n  \"\",\n  \"KONTEXT\",\n  '- Heute ' + berlinNow + ' Europe/Berlin',\n  \"- Standort Berlin\",\n  \"\",\n  \"EVIDENZREGEL\",\n  \"- Kalender, Gmail, CRM und externe Fakten niemals erfinden. Nur Tool Daten oder klar markierte Nutzeraussagen verwenden.\",\n  \"- Wenn count=0 oder keine Termine oder E Mails gefunden wurden, kurz sagen oder bei PreMeeting FINAL: skip.\",\n  \"- Wenn ein Tool Fehler meldet, erkl\u00e4re kurz den Fehler und wiederhole denselben Tool Versuch nicht blind.\",\n  \"\",\n  \"TOOLS\",\n  \"Gmail mit gmail_search query maxResults, gmail_read messageId, gmail_draft\",\n  \"Kalender mit calendar_get timeMin timeMax maxResults im ISO Format. Liest Primary Project und Privat.\",\n  \"Notion CRM mit notion_search, notion_read, notion_update, notion_create\",\n  \"Memory Markdown memory repo mit memory_write target=memory|decision|daily, content=...\",\n  \"\",\n  \"AUSGABEFORMAT IST ZWINGEND\",\n  \"Wenn Tool n\u00f6tig, antworte ausschlie\u00dflich so\",\n  \"ACTION: <toolname>\",\n  \"PARAMS: key=value, key2=value2\",\n  \"\",\n  \"Finale Antwort\",\n  \"FINAL: <kurzer Text>\",\n  \"\",\n  \"WICHTIG\",\n  \"- Nie ACTION und FINAL mischen.\",\n  \"- Wenn ein proaktiver Job nichts Sinnvolles zu melden hat, antworte mit FINAL: skip damit der Workflow die Nachricht unterdr\u00fcckt.\",\n  \"- PreMeeting nur Termine melden, die in 10 bis 30 Minuten starten und vom Kalender Tool als nicht bereits gemeldet zur\u00fcckgegeben werden.\",\n  \"- Niemals Pseudo Termine erfinden. Nur was im Kalender Tool Ergebnis steht.\"\n].join('\\n');\nconst promptSpacer = String.fromCharCode(10) + String.fromCharCode(10);\nconst output = {\n  message_text: ((data.prompt || '') + promptSpacer + windowHint).trim(),\n  sessionId: 'opsAgent-' + (data.scenario || 'heartbeat') + '-' + nowParts.year + pad(nowParts.month) + pad(nowParts.day),\n  username: 'the user',\n  chat_id: 'YOUR_TELEGRAM_CHAT_ID',\n  input_was_voice: false,\n  source: 'heartbeat',\n  replyMode: 'telegram_push',\n  scenario: data.scenario || '',\n  debugEnabled: false,\n  systemPrompt\n};\nreturn [ { json: output } ];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        560,
        1728
      ],
      "id": "single-build-heartbeat-agent-request",
      "name": "Build Heartbeat Agent Request"
    },
    {
      "parameters": {
        "jsCode": "const data = $input.first().json || {};\nconst hasChatId = Boolean(data.chat_id);\nconst output = {\n  message_text: data.message_text || data.chatInput || data.prompt || '',\n  original_message_text: data.original_message_text || data.message_text || data.chatInput || data.prompt || '',\n  slash_command: data.slash_command || '',\n  sessionId: String(data.sessionId || data.user_id || 'default'),\n  username: data.username || 'the user',\n  rawSystemPrompt: data.systemPrompt || data.rawSystemPrompt || '',\n  chat_id: data.chat_id || '',\n  input_was_voice: Boolean(data.input_was_voice),\n  source: data.source || (hasChatId ? 'telegram' : 'chat'),\n  replyMode: data.replyMode || (hasChatId ? 'telegram' : 'chat'),\n  scenario: data.scenario || '',\n  input_type: data.input_type || '',\n  force_voice_reply: Boolean(data.force_voice_reply),\n  debugEnabled: Boolean(data.debugEnabled),\n  lastToolWorkflow: data.lastToolWorkflow || '',\n  lastToolOperation: data.lastToolOperation || '',\n  lastToolSummary: data.lastToolSummary || '',\n  lastToolCount: typeof data.lastToolCount === 'number' ? data.lastToolCount : 0,\n  lastToolSuccess: data.lastToolSuccess !== false\n};\nreturn [ { json: output } ];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        784,
        1584
      ],
      "id": "single-agent-init-context",
      "name": "Agent - Init Context"
    },
    {
      "parameters": {
        "url": "https://api.github.com/repos/YOUR_GITHUB_USER/YOUR_MEMORY_REPO/contents/Assistant/Memory.md",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "ref",
              "value": "main"
            }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1008,
        1584
      ],
      "id": "single-agent-read-memory-file",
      "name": "Agent - Read Memory File",
      "continueOnFail": true
    },
    {
      "parameters": {
        "url": "https://api.github.com/repos/YOUR_GITHUB_USER/YOUR_MEMORY_REPO/contents/Assistant/Decisions.md",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "ref",
              "value": "main"
            }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1232,
        1584
      ],
      "id": "single-agent-read-decisions-file",
      "name": "Agent - Read Decisions File",
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const ctx = $('Agent - Init Context').item.json || {};\nconst memoryGh = $('Agent - Read Memory File').item.json || {};\nconst decisionsGh = $input.first().json || {};\nconst NL = String.fromCharCode(10);\nlet memoryText = '';\nlet decisionsText = '';\nlet memoryStatus = 'ok';\nlet decisionsStatus = 'ok';\nif (memoryGh.content && !memoryGh.error) {\n  try {\n    memoryText = Buffer.from(memoryGh.content, 'base64').toString('utf-8').trim();\n  } catch (error) {\n    memoryText = '';\n    memoryStatus = 'decode_error';\n  }\n} else {\n  memoryStatus = memoryGh.error ? 'read_error' : 'empty';\n}\nif (decisionsGh.content && !decisionsGh.error) {\n  try {\n    decisionsText = Buffer.from(decisionsGh.content, 'base64').toString('utf-8').trim();\n  } catch (error) {\n    decisionsText = '';\n    decisionsStatus = 'decode_error';\n  }\n} else {\n  decisionsStatus = decisionsGh.error ? 'read_error' : 'empty';\n}\nconst memoryBlock = memoryText\n  ? NL + NL + ['=== PERSONAL MEMORY Assistant/Memory.md aus Markdown memory ===', memoryText, '=== ENDE PERSONAL MEMORY ==='].join(NL)\n  : NL + NL + ['=== PERSONAL MEMORY ===', 'Memory ist noch leer oder konnte nicht gelesen werden.', '=== ENDE PERSONAL MEMORY ==='].join(NL);\nconst decisionsBlock = decisionsText\n  ? NL + NL + ['=== DECISIONS Assistant/Decisions.md aus Markdown memory ===', decisionsText, '=== ENDE DECISIONS ==='].join(NL)\n  : NL + NL + ['=== DECISIONS ===', 'Decisions ist noch leer oder konnte nicht gelesen werden.', '=== ENDE DECISIONS ==='].join(NL);\nconst writeRules = [\n  '',\n  '',\n  'Nutze diese Informationen aktiv, aber speichere mit 
Pro

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

How this works

This workflow enables seamless AI-powered conversations across messaging platforms, allowing users to interact with an intelligent agent that handles text, voice, and routed queries effortlessly. It's ideal for developers or teams building chatbots for customer support or personal assistants, delivering quick, context-aware responses without manual intervention. The key step involves normalising inputs from triggers like Telegram or chat interfaces, then routing them through OpenAI for transcription and intelligent processing, ensuring reliable handling of diverse message types including voice files.

Use this workflow when creating event-driven AI agents for real-time interactions on platforms like Telegram, especially if you need voice transcription via OpenAI or integration with Gmail for notifications. Avoid it for simple, non-AI automations or high-volume enterprise setups requiring custom scaling, as its 82 nodes suit moderate complexity. Common variations include adding HTTP requests for external API calls or swapping triggers for webhooks to adapt to different input sources.

About this workflow

AI Agent Workflow. Uses telegramTrigger, chatTrigger, telegram, openAi. Event-driven trigger; 82 nodes.

Source: https://github.com/bpnace/AI-Agent-Workflow/blob/main/workflows/ai-agent-workflow.sanitized.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Transform your salon/service business with this streamlined Telegram automation system featuring Claude integration, zero-setup database management, and intelligent conversation handling. Claude MCP I

Redis, Agent Tool, Google Calendar +10
AI & RAG

This workflow contains community nodes that are only compatible with the self-hosted version of n8n.

Agent, OpenAI Chat, OpenAI +8
AI & RAG

AI-Journaling. Uses telegramTrigger, telegram, openAi, lmChatOpenAi. Event-driven trigger; 36 nodes.

Telegram Trigger, Telegram, OpenAI +5
AI & RAG

Pitch Paul. Uses lmChatOpenRouter, telegram, outputParserStructured, supabaseTool. Event-driven trigger; 33 nodes.

OpenRouter Chat, Telegram, Output Parser Structured +10
AI & RAG

Build a personal Telegram bot that looks up English vocabulary and saves every entry to Notion — supporting text, voice, and photo input.

Telegram Trigger, Telegram, HTTP Request +5