AutomationFlowsAI & RAG › Bridge Pm Bot

Bridge Pm Bot

bridge_pm_bot. Uses telegramTrigger, postgres, httpRequest. Event-driven trigger; 23 nodes.

Event trigger★★★★☆ complexity23 nodesTelegram TriggerPostgresHTTP Request
AI & RAG Trigger: Event Nodes: 23 Complexity: ★★★★☆ Added:

This workflow follows the HTTP Request → Postgres 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": "bridge_pm_bot",
  "nodes": [
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "id": "node_trigger_tg_pm",
      "name": "Telegram: user DM",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        200
      ],
      "disabled": true
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 0 */6 * * *"
            }
          ]
        }
      },
      "id": "node_trigger_daily",
      "name": "Schedule: daily",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        400
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 0 10 * * 1"
            }
          ]
        }
      },
      "id": "node_trigger_weekly",
      "name": "Schedule: weekly",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        600
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "bridge-pm-invoke",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "node_trigger_webhook_pm",
      "name": "Webhook: invoke",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        800
      ]
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500 V2 Task Envelope: task_id propagation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst _env = (typeof $input !== 'undefined' && $input.all()[0].json) || {};\nconst _envBody = _env.body || {};\nconst _inherited_task_id = _envBody.task_id || null;\nconst _gen_task_id = () => 'tid-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9);\nconst _task_id = _inherited_task_id || _gen_task_id();\n// \u2500\u2500 End V2 Task Envelope \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst src = $input.all()[0];\n// \u2500\u2500 Loop prevention guard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst _incomingBody = (src.json && src.json.body) ? src.json.body : {};\nconst _attemptCount = (_incomingBody.attempt_count !== undefined)\n    ? parseInt(_incomingBody.attempt_count, 10) || 0 : 0;\nif (_attemptCount >= 1) {\n    return [{ json: { loop_detected: true, original_task: _incomingBody,\n        bot_name: 'pm',\n        task: 'loop_escalation', user_text: null, chat_id: null, from_agent: 'loop_guard',\n        now_iso: new Date().toISOString(), daily_spec: [], weekly_spec: [] } }];\n}\n// \u2500\u2500 End loop prevention \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// \u2500\u2500 Deduplication guard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst messageId = (src.json && src.json.message && src.json.message.message_id)\n    ? String(src.json.message.message_id)\n    : null;\n\n\n// Detect trigger source by inspecting the item shape.\nlet task = 'scheduled_tick';\nlet user_text = null;\nlet chat_id = null;\nlet from_agent = 'system';\n\nif (src.json && src.json.message && src.json.message.chat) {\n    // Telegram trigger\n    task = 'user_dm';\n    let _utext = src.json.message.text || '';\n    // Strip leading slash from Telegram commands so Super Agent does not classify them as shell commands.\n    if (_utext.startsWith('/')) {\n        const parts = _utext.slice(1).split(/\\s+/);\n        const cmd = (parts[0] || '').toLowerCase();\n        const rest = parts.slice(1).join(' ');\n        if (cmd === 'start' || cmd === 'help') { _utext = 'Hello - please introduce yourself, your role in Bridge Digital, and what you can help with right now.'; }\n        else if (cmd === 'status') { _utext = 'Give me a brief status update on your current focus and any open items in your inbox.'; }\n        else { _utext = (cmd + ' ' + rest).trim(); }\n    }\n    user_text = _utext;\n    chat_id = src.json.message.chat.id;\n    from_agent = 'user';\n} else if (src.json && src.json.body && src.json.body.task) {\n    // Webhook trigger\n    task = src.json.body.task;\n    user_text = src.json.body.message || null;\n    from_agent = src.json.body.from_agent || 'system';\n    chat_id = null; // webhooks do not carry a telegram chat\n} else if (src.json && src.json.timestamp) {\n    // Schedule trigger\n    task = 'scheduled_tick';\n    user_text = null;\n    from_agent = 'system';\n}\n\nreturn [{ json: { message_id: messageId,\n    bot_name: \"pm\",\n    task,\n    user_text,\n    chat_id,\n    from_agent,\n    now_iso: new Date().toISOString(),\n    daily_spec: [{\"hour\": 9, \"minute\": 0, \"task_name\": \"daily_project_pulse\"}],\n    weekly_spec: [{\"weekday\": 1, \"hour\": 10, \"minute\": 0, \"task_name\": \"weekly_project_review\"}],\n    task_id: _task_id,\n} }];"
      },
      "id": "node_build_task",
      "name": "Build task payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        400
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT (value = 'true') AS enabled FROM bridge.system_limits WHERE key = 'pm_bot_enabled';",
        "options": {}
      },
      "id": "node_read_enabled",
      "name": "Read enabled flag",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        680,
        400
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "enabled_check",
              "leftValue": "={{ $json.enabled }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "node_if_enabled",
      "name": "If enabled",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        900,
        400
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COALESCE(jsonb_agg(row_to_json(m) ORDER BY m.priority_rank, m.created_at), '[]'::jsonb)        AS open_inbox FROM (   SELECT memo_id, from_agent, memo_type, priority, subject, body_json,          created_at, related_lead_id,          CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1                        WHEN 'normal' THEN 2 ELSE 3 END AS priority_rank   FROM bridge.agent_memos   WHERE to_agent IN ('pm', 'all')     AND status = 'open'   LIMIT 20 ) m;",
        "options": {}
      },
      "id": "node_fetch_inbox",
      "name": "Fetch open inbox",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1120,
        400
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT jsonb_build_object(\n    'now_utc', NOW(),\n    'kpi_last_24h', (\n        SELECT jsonb_build_object(\n            'leads_created',     (SELECT COUNT(*)::int FROM bridge.leads WHERE created_at >= NOW() - INTERVAL '24 hours'),\n            'demos_built',       (SELECT COUNT(*)::int FROM bridge.website_projects WHERE created_at >= NOW() - INTERVAL '24 hours'),\n            'outreach_sent',     (SELECT COUNT(*)::int FROM bridge.outreach_messages WHERE direction='outbound' AND sent_at >= NOW() - INTERVAL '24 hours'),\n            'interested_replies',(SELECT COUNT(*)::int FROM bridge.leads WHERE marketing_status='Interested' AND updated_at >= NOW() - INTERVAL '24 hours'),\n            'invoices_paid',     (SELECT COUNT(*)::int FROM bridge.leads WHERE finance_status='Paid' AND updated_at >= NOW() - INTERVAL '24 hours'),\n            'expenses_usd',      (SELECT ROUND(COALESCE(SUM(amount_usd),0)::numeric, 4) FROM bridge.expenses WHERE occurred_at >= NOW() - INTERVAL '24 hours')\n        )\n    ),\n    'kpi_last_7d', (\n        SELECT jsonb_build_object(\n            'leads_created',     (SELECT COUNT(*)::int FROM bridge.leads WHERE created_at >= NOW() - INTERVAL '7 days'),\n            'demos_built',       (SELECT COUNT(*)::int FROM bridge.website_projects WHERE created_at >= NOW() - INTERVAL '7 days'),\n            'outreach_sent',     (SELECT COUNT(*)::int FROM bridge.outreach_messages WHERE direction='outbound' AND sent_at >= NOW() - INTERVAL '7 days'),\n            'interested_replies',(SELECT COUNT(*)::int FROM bridge.leads WHERE marketing_status='Interested' AND updated_at >= NOW() - INTERVAL '7 days'),\n            'invoices_paid',     (SELECT COUNT(*)::int FROM bridge.leads WHERE finance_status='Paid' AND updated_at >= NOW() - INTERVAL '7 days'),\n            'expenses_usd',      (SELECT ROUND(COALESCE(SUM(amount_usd),0)::numeric, 4) FROM bridge.expenses WHERE occurred_at >= NOW() - INTERVAL '7 days')\n        )\n    ),\n    'urgent_memos_across_ecosystem', (\n        SELECT COALESCE(jsonb_agg(row_to_json(m)), '[]'::jsonb)\n        FROM (\n            SELECT memo_id, from_agent, to_agent, subject, priority, created_at, body_json\n            FROM bridge.agent_memos\n            WHERE status='open' AND priority IN ('urgent','high')\n            ORDER BY priority, created_at\n            LIMIT 15\n        ) m\n    ),\n    'researcher_proposals_open', (\n        SELECT COALESCE(jsonb_agg(row_to_json(p)), '[]'::jsonb)\n        FROM (\n            SELECT memo_id, subject, body_json, created_at\n            FROM bridge.agent_memos\n            WHERE from_agent='researcher' AND memo_type='proposal' AND status='open'\n            ORDER BY created_at\n            LIMIT 10\n        ) p\n    ),\n    'campaign_priorities', (\n        SELECT COALESCE(jsonb_agg(row_to_json(c)), '[]'::jsonb)\n        FROM (\n            SELECT campaign_target_id, niche, city, priority, active_flag,\n                   daily_lead_target, daily_website_limit\n            FROM bridge.campaign_targets\n            ORDER BY priority DESC, niche, city\n            LIMIT 20\n        ) c\n    )\n) AS context;",
        "options": {}
      },
      "id": "node_fetch_context",
      "name": "Fetch bot context",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1120,
        600
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const _buildTask = $('Build task payload').first().json;\nif (_buildTask.loop_detected) {\n    return [{ json: {\n        message: JSON.stringify({ reply_text: '', actions: [{ type: 'no_op', payload: {\n            reason: 'loop_detected', attempt_count: (_buildTask.original_task && _buildTask.original_task.attempt_count) || 1\n        }}]}),\n        session_id: 'bridge-loop-guard-' + Date.now(),\n        task_kind: 'loop_escalation', user_chat_id: null,\n        bot_name: _buildTask.bot_name || 'unknown', _pre_formed: true\n    }}];\n}\n\nconst task = $('Build task payload').first().json;\n// \u2500\u2500 Memory auto-block check (non-blocking) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet _autoBlockActive = false;\nconst _querySubject = task.user_text || task.subject || '';\nif (_querySubject && _querySubject.length > 5) {\n    try {\n        const _memCheck = await $http.post(\n            `${$env.SUPER_AGENT_URL || 'https://super-agent-production.up.railway.app'}/webhook/memory-query`,\n            { project_name: _querySubject.slice(0, 100), api_key: $env.N8N_API_KEY || '' }\n        );\n        if (_memCheck && _memCheck.data && _memCheck.data.auto_block === true) {\n            _autoBlockActive = true;\n            return [{ json: {\n                message: JSON.stringify({\n                    reply_text: `\ud83d\udeab AUTO-BLOCK: Project has been rejected ${_memCheck.data.rejection_count} time(s). Route to CRO then CEO for override.`,\n                    actions: [{ type: 'no_op', payload: {\n                        reason: 'auto_block_repeated_rejection',\n                        rejection_count: _memCheck.data.rejection_count,\n                        similar_projects: _memCheck.data.similar_projects\n                    }}]\n                }),\n                session_id: `bridge-auto-block-${Date.now()}`,\n                task_kind: 'auto_block', user_chat_id: task.chat_id,\n                bot_name: task.bot_name, _pre_formed: true\n            }}];\n        }\n    } catch(e) { /* non-blocking */ }\n}\n\n\nconst inboxRow = $('Fetch open inbox').first().json || {};\nconst inbox = Array.isArray(inboxRow.open_inbox) ? inboxRow.open_inbox : [];\nconst ctxRow = $('Fetch bot context').first().json || {};\nconst ctx = ctxRow.context ? [ctxRow.context] : [ctxRow];\n\n// \u2500\u2500 Structured brief normalisation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// When user_text is a short, unstructured brief (< 60 words), wrap it in a\n// structured envelope so downstream agents (Website bot, Programmer bot) get\n// well-formed input rather than raw free text. This prevents 'had trouble\n// producing a structured response' failures.\nfunction normaliseBrief(text) {\n    if (!text) return text;\n    const words = text.trim().split(/\\s+/);\n    if (words.length >= 60) return text; // already verbose enough\n    const lc = text.toLowerCase();\n    // Detect website/landing-page briefs\n    if (lc.includes('landing page') || lc.includes('website') || lc.includes('site')) {\n        return JSON.stringify({\n            type: 'website_brief',\n            raw_brief: text,\n            instructions: 'Parse the raw_brief and fill in as many fields as possible: niche, location, goal, sections, colour_scheme, call_tracking, deadline. Then pass the structured payload to the Website bot.'\n        });\n    }\n    return text;\n}\nconst normalised_user_text = normaliseBrief(task.user_text);\n\n\n\nconst globalProtocol = `\n\nGLOBAL OPERATING PROTOCOL (non-negotiable):\n1. ALWAYS respond in valid JSON only. No prose, no markdown outside JSON strings.\n2. EXACT response structure: {\"reply_text\": \"...\", \"actions\": [...]}\n3. reply_text: human-readable Telegram message. Brief, role-appropriate.\n4. actions: array of {type, payload} objects. Empty array if no actions.\n5. NEVER include PLACEHOLDER, PASTE_, TBD, INSERT_HERE values in any field.\n6. NEVER mention internal tools (n8n, shell, SQL, workflow IDs) in reply_text.\n7. If required data is missing \u2192 return no_op with status \"need_input\" + list exact missing fields.\n8. If blocked \u2192 escalate to Chief of Staff. NEVER retry autonomously.\n9. MAX_RETRIES = 1. If attempt_count >= 1 in incoming body_json \u2192 return no_op immediately.\n`;\n\nconst system = \"You are Bridge_PM_BOT, the Project Manager super-agent inside Bridge Digital Solutions.\\n\\nROLE\\nYou are the execution planning and project management agent. You break opportunities into actionable steps, define timelines and milestones, assign responsibilities across agents, identify dependencies and risks, and produce execution roadmaps that the rest of the multi-agent ecosystem can deliver against.\\n\\nPRIMARY OBJECTIVES\\n1. Convert approved opportunities into concrete project plans.\\n2. Maintain timelines, milestones, and deliverables.\\n3. Assign responsibilities to the right agents.\\n4. Identify dependencies, blockers, and risks before they become problems.\\n5. Track progress and surface variance early.\\n\\nSECONDARY OBJECTIVES\\n1. Coordinate budget-aware execution with Finance.\\n2. Optimise sequencing across multiple parallel projects.\\n3. Improve estimation accuracy over time using past project outcomes.\\n4. Support cleanup/closure cycles after project stages.\\n\\nCORE FUNCTIONS\\nProject intake from CEO, project planning into tasks/milestones/deadlines, resource assignment to operational agents, dependency mapping, risk identification, progress tracking, status reporting, and re-planning when scope changes.\\n\\nWHAT YOU MUST PRODUCE\\nFor each project: project_id, name, sponsor, objective, success_criteria, milestones with target_dates, task_breakdown (task_id, description, owner_agent, depends_on, eta, status), risks (description, severity, mitigation), budget_estimate with assumptions, progress_status (on_track/at_risk/blocked/delayed), and next_actions.\\nFor status updates: which milestones hit/missed/pushed, who is blocked, what needs decision, recommendations.\\n\\nDECISION PRINCIPLES\\nBe conservative with estimates. Prefer small sequenced deliverables over big-bang plans. Surface dependencies early. Match each task to the cheapest competent agent (cost discipline). Re-plan early when reality diverges from plan. Keep plans short, current, and execution-ready.\\n\\nAUTHORITY\\nYou may assign tasks to operational agents, request status updates, escalate blockers and overdue items, recommend re-prioritisation to CEO and Chief of Staff, and request budget authorisation from Finance for additional resources. You may NOT approve major budget changes without Finance + CEO sign-off, approve strategic pivots (CEO authority), or override security decisions (Chief Sec Off authority).\\n\\nINTERACTION RULES\\nYou collaborate with Bridge_CEO_BOT for project sponsorship and strategic alignment, Bridge_Chief_Of_Staff_bot for cross-functional coordination, Bridge_Researcher_bot for opportunity context, Bridge_Chief_Sec_Off_bot when projects touch security/exposure/compliance, Bridge_Cleaner_bot for closure cycles, and Finance for budget validation and cost monitoring.\\n\\nESCALATE WHEN timeline slippage exceeds tolerance, multiple agents are blocked on the same dependency, budget overrun is imminent, risks materialise above tolerance, scope changes materially, or a project should be paused/redirected/cancelled.\\n\\nOUTPUT STYLE\\nStructure outputs as: current_state, progress_since_last_check, blockers, required_decisions, next_actions (with owners and deadlines), updated_plan if material changes.\\n\\nSUCCESS DEFINITION\\nYou are successful when projects ship on time, on budget, and at the planned quality, with minimal surprise to leadership.\\n\\nREVENUE GATE (mandatory)\\nBefore emitting any escalate action OR any memo with to_agent=ceo and memo_type=approval_request, you MUST first post a memo of memo_type=revenue_review_request to bizdev. Include in body_json: project_id (stable string), project_summary, target_customer_hypothesis, proposed_monetisation, and deferred_action (the action you would have emitted). Wait for the corresponding resolved revenue_review from bizdev before re-emitting the deferred action. The Parse Response code in this workflow will auto-reroute approval actions to bizdev when no resolved revenue_review exists for the project.\\n\\n\\nCTO ARCHITECTURE GATE (mandatory):\\nYou are the execution manager. The CTO designs architecture; you execute it.\\nNEVER assign any build task to Website Bot or Programmer Bot without a CTO architecture plan.\\n\\nBEFORE every build assignment:\\n1. Check your open_inbox for a memo of memo_type='cto_review_complete' for this project.\\n2. If NOT found \u2192 send memo_type='cto_review_request' to_agent='cto' with project details. Do NOT assign to builders until cto_review_complete arrives.\\n3. If FOUND \u2192 extract architecture_plan from body_json and use it:\\n   - Use cost_strategy.model_recommendation for all build assignments\\n   - Use architecture_plan.stack and tools in your task briefs\\n   - Use architecture_plan.estimated_build_time_days for milestones\\n   - If recommendation = REJECT \u2192 escalate to COS immediately, do NOT build\\n   - If recommendation = MODIFY \u2192 revise brief and re-request CTO review\\n\\nPOST-BUILD VALIDATION:\\nAfter build completes, send memo_type='build_validation_request' to_agent='cto' with deploy URL and build output. CTO validates before you mark the milestone complete.\\nYou do NOT have authority to declare a build successful without CTO sign-off.\";\n\nconst contextBlock = JSON.stringify({\n    now: task.now_iso,\n    bot: task.bot_name,\n    task_kind: task.task,\n    open_inbox: inbox,\n    bot_context: ctx,\n    daily_cadence: task.daily_spec,\n    weekly_cadence: task.weekly_spec,\n}, null, 2);\n\nconst taskBlock = task.task === 'user_dm'\n    ? `The user sent you this message on Telegram:\\n${normalised_user_text}\\n\\nRespond helpfully and consider issuing actions if appropriate.`\n    : task.task === 'scheduled_tick'\n      ? `This is a scheduled cadence run. Decide what to do given the current context, open memos, and your role.`\n      : `An inter-agent invocation arrived from '${task.from_agent}': ${normalised_user_text || 'no message body'}. Decide how to respond.`;\n\nconst outputGuard = `\\n\\n[OUTPUT FORMAT]\\nReturn ONLY a JSON object (no prose, no markdown fences):\\n{\\n  \"reply_text\": \"<what to send back via Telegram; empty string when a scheduled run should stay silent>\",\\n  \"actions\": [\\n    {\"type\": \"memo\",     \"payload\": {\"to_agent\": \"researcher|chief_of_staff|cso|ceo|cleaner|all\", \"memo_type\": \"status|proposal|directive|question|decision\", \"priority\": \"urgent|high|normal|low\", \"subject\": \"...\", \"body_json\": {...}}},\\n    {\"type\": \"archive\",  \"payload\": {\"memo_id\": \"<uuid-from-open-inbox>\", \"reason\": \"...\"}},\\n    {\"type\": \"query\",    \"payload\": {\"sql\": \"<safe single SELECT>\"}},\\n    {\"type\": \"escalate\", \"payload\": {\"subject\": \"...\", \"body_json\": {...}}},\\n    {\"type\": \"cleanup\",  \"payload\": {\"slug\": \"...\", \"reason\": \"...\"}},\\n    {\"type\": \"no_op\",    \"payload\": {\"reason\": \"...\"}}\\n  ]\\n}\\nKeep actions <= 5. Never include non-whitelisted action types. User-facing commentary belongs in reply_text, not in alert actions.`;\n\nconst fullMessage = `${system}${globalProtocol}\\n\\n[CONTEXT]\\n${contextBlock}\\n\\n[TASK]\\n${taskBlock}${outputGuard}`;\n\nreturn [{ json: {\n    message: fullMessage,\n    session_id: `bridge-${task.bot_name}-${task.now_iso.slice(0,16).replace(/[:T-]/g,'')}`,\n    task_kind: task.task,\n    user_chat_id: task.chat_id,\n    bot_name: task.bot_name,\n} }];"
      },
      "id": "node_assemble_prompt",
      "name": "Assemble prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1340,
        400
      ]
    },
    {
      "id": "node_chat_direct",
      "name": "super-agent /webhook/bot-engine",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1560,
        400
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SUPER_AGENT_URL || 'https://super-agent-production.up.railway.app' }}/webhook/bot-engine",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"bot_name\": {{ JSON.stringify($json.bot_name || \"unknown\") }}, \"task_block\": {{ JSON.stringify($json.message || \"\") }}, \"session_id\": {{ JSON.stringify($json.session_id || \"default\") }}, \"task_kind\": {{ JSON.stringify($json.task || \"agent_invoke\") }}, \"api_key\": {{ JSON.stringify($env.N8N_API_KEY || \"\") }} }",
        "options": {
          "timeout": 180000,
          "response": {
            "response": {
              "neverError": true
            }
          },
          "retryOnFail": true,
          "maxTries": 3,
          "waitBetweenTries": 4000
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500 Pre-formed short-circuit (loop guard) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst _upstream = $('Assemble prompt').first().json;\nif (_upstream && _upstream._pre_formed) {\n    let _pfActions;\n    try { _pfActions = JSON.parse(_upstream.message).actions; } catch(e) { _pfActions = []; }\n    return [{ json: {\n        reply_text: '',\n        actions: [{ type: 'no_op', risk: 'low',\n            payload: (_pfActions[0] && _pfActions[0].payload) || { reason: 'loop_guard' } }],\n        bot_name: _upstream.bot_name, user_chat_id: _upstream.user_chat_id,\n        task_kind: 'loop_escalation', model_used: 'loop_guard', parse_error: null\n    }}];\n}\n\n// \u2500\u2500 Standard parse + risk + validation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst raw = $json.response || '';\nconst riskMap = { \"memo\": \"low\", \"archive\": \"low\", \"no_op\": \"low\",\n                  \"escalate\": \"low\", \"query\": \"medium\", \"cleanup\": \"high\" };\nconst defaultRisk = 'low';\n\nlet parsed = null, parseError = null;\ntry {\n    let cleaned = raw.trim().replace(/^```(?:json)?/, '').replace(/```$/, '').trim();\n    parsed = JSON.parse(cleaned);\n} catch (e) { parseError = e.message; }\nif (!parsed) {\n    try { const m = raw.match(/\\{[\\s\\S]*\\}/); if (m) parsed = JSON.parse(m[0]); } catch (e) {}\n}\n\nlet reply_text = '', actions = [];\nif (parsed && typeof parsed === 'object' && parsed.reply_text !== undefined) {\n    reply_text = typeof parsed.reply_text === 'string' ? parsed.reply_text : '';\n    actions = Array.isArray(parsed.actions) ? parsed.actions.slice(0, 5) : [];\n} else {\n    reply_text = '';\n    actions = [{ type: 'no_op', payload: {\n        reason: 'json_parse_failed',\n        parse_error: parseError || 'no structured response from model',\n        raw_preview: String(raw).slice(0, 200)\n    }}];\n}\n\n// \u2500\u2500 Placeholder validation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst BANNED = ['PASTE_', 'TBD', 'PLACEHOLDER', 'INSERT_HERE', 'YOUR_URL'];\nconst hasPlaceholder = (str) => BANNED.some(p => String(str).includes(p));\nif (hasPlaceholder(reply_text) || actions.some(a => hasPlaceholder(JSON.stringify(a)))) {\n    reply_text = '[Output validation failed \u2014 response contained placeholder values. Please provide the actual value.]';\n    actions = [{ type: 'no_op', payload: { reason: 'placeholder_detected' }}];\n}\n\n// \u2500\u2500 Internal tool leakage scrubbing (non-blocking) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst leakageMap = {\n    'n8n workflow creation': 'automation system modification',\n    'n8n workflow modification': 'automation system modification',\n    'shell command (destructive)': 'system operation',\n    'shell command': 'system operation',\n    'SQL query': 'data operation',\n};\nfor (const [internal, safe] of Object.entries(leakageMap)) {\n    if (reply_text.toLowerCase().includes(internal.toLowerCase()))\n        reply_text = reply_text.replace(new RegExp(internal, 'gi'), safe);\n}\n\n// \u2500\u2500 Risk annotation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst annotated = actions.map(a => {\n    const type = (a && typeof a.type === 'string') ? a.type : 'no_op';\n    let risk = riskMap[type] !== undefined ? riskMap[type] : defaultRisk;\n    return { type, risk, payload: a.payload || {} };\n});\n\nconst upstream = $('Assemble prompt').first().json;\n\n// \u2500\u2500 Telegram personality formatting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction _formatTgMsg(raw, taskKind) {\n  if (!raw || raw.length < 3) return raw;\n\n  // Parse ISO timestamp for the footer\n  const _ts = new Date().toLocaleTimeString('en-GB', {hour:'2-digit',minute:'2-digit',timeZone:'UTC'}) + ' UTC';\n\n  // Build structured header\n  const _kind = (taskKind || 'report').replace(/_/g,' ');\n  let header = '\ud83d\udccb *Project Manager*\\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\\n';\n\n  // Transform the body: bold lines that look like labels, add section icons\n  let body = raw\n    .replace(/\\*\\*/g, '*')           // normalise bold markers\n    .replace(/^(#+)\\s+(.+)$/gm, (_, h, t) => '*' + t + '*')   // ## heading \u2192 bold\n    .replace(/^[-\u2022]\\s+/gm, '  \ud83d\udccc ')                        // bullet \u2192 section icon\n    .replace(/^(\\w[^:\\n]{2,40}):\\s*(.+)$/gm, '*$1:* $2')    // Key: val \u2192 bold key\n    .replace(/ERROR|FAILED|CRITICAL|BLOCKED/g, '\u23f0 $&')     // flag errors\n    .replace(/SUCCESS|COMPLETE|DONE|APPROVED/gi, '\u2705 $&')     // flag successes\n    .replace(/ACTION:|action:/g, '\ud83c\udfc1 *ACTION:*')          // highlight actions\n    ;\n\n  // Escape Markdown special chars NOT inside existing bold spans\n  // (Telegram Markdown v1: escape [ ] only)\n  body = body.replace(/([\\[\\]])/g, '\\\\$1');\n\n  const footer = '\\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\\n_\ud83d\uddd3\ufe0f PM \u00b7 Bridge OS_ \u00b7 _' + _ts + '_';\n\n  return header + body + footer;\n}\n\n// Apply formatting to reply_text if there is one\nif ($json && $json.reply_text && $json.reply_text.length > 3) {\n  const _taskKind = $json.task_kind || upstream && upstream.task_kind || '';\n  $json.reply_text = _formatTgMsg($json.reply_text, _taskKind);\n}\n\nreturn [{ json: {\n    reply_text, actions: annotated,\n    bot_name: upstream.bot_name, user_chat_id: upstream.user_chat_id,\n    task_kind: upstream.task_kind, model_used: $json.model_used || 'unknown',\n    parse_error: parseError\n}}];"
      },
      "id": "node_parse_response",
      "name": "Parse response + risk tag",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1780,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "has_reply",
              "leftValue": "={{ ($json.reply_text || '').trim().length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "node_if_has_reply",
      "name": "If has reply",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2000,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.telegram.org/bot{{$env.BRIDGE_PM_BOT_TOKEN || $env.Bridge_CEO_BOT}}/sendMessage",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json; charset=utf-8"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"chat_id\": {{ $('Parse response + risk tag').first().json.user_chat_id || $env.BRIDGE_ADMIN_TELEGRAM_CHAT_ID }}, \"text\": \"{{ $('Parse response + risk tag').first().json.reply_text || '(no reply)' }}\" , \"parse_mode\": \"Markdown\" }",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node_reply_tg",
      "name": "Reply on Telegram",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2880,
        400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://super-agent-production.up.railway.app/memory/ingest",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Memory-Secret",
              "value": "={{$env.MEMORY_INGEST_SECRET}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"memories\": [ { \"content\": {{ JSON.stringify(($('Parse response + risk tag').first().json.reply_text || '(no reply)') + ' [actions=' + (($('Parse response + risk tag').first().json.actions || []).map(a => a.type).join(',')) + ']') }}, \"memory_type\": \"decision\", \"importance\": 3, \"source\": \"bridge_pm_bot\"}]}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node_memory_ingest",
      "name": "Memory ingest",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3100,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "const p = $json;\nconst out = [];\nfor (const a of (p.actions || [])) {\n    out.push({ json: {\n        action_type: a.type,\n        action_risk: a.risk,\n        action_payload: a.payload,\n        bot_name: p.bot_name,\n        user_chat_id: p.user_chat_id,\n        task_kind: p.task_kind,\n        model_used: p.model_used,\n        reply_text: p.reply_text,\n    }});\n}\n// Always emit at least one item so the reply path runs even when no actions.\nif (out.length === 0) {\n    out.push({ json: {\n        action_type: 'no_op',\n        action_risk: 'low',\n        action_payload: { reason: 'no actions from LLM' },\n        bot_name: p.bot_name,\n        user_chat_id: p.user_chat_id,\n        task_kind: p.task_kind,\n        model_used: p.model_used,\n        reply_text: p.reply_text,\n    }});\n}\nreturn out;"
      },
      "id": "node_split_actions",
      "name": "Split actions",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        400
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action_risk }}",
                    "rightValue": "low",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "low"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action_risk }}",
                    "rightValue": "medium",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "medium"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action_risk }}",
                    "rightValue": "high",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "high"
            }
          ]
        },
        "options": {
          "allMatchingOutputs": false,
          "fallbackOutput": 2
        }
      },
      "id": "node_switch_risk",
      "name": "Switch by risk",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        2220,
        400
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "\nWITH input AS (\n  SELECT $1::jsonb AS p\n),\nmemo_insert AS (\n  INSERT INTO bridge.agent_memos\n    (from_agent, to_agent, memo_type, priority, subject, body_json, related_lead_id)\n  SELECT\n    $2,\n    COALESCE(p->>'to_agent','all'),\n    COALESCE(p->>'memo_type','status'),\n    COALESCE(p->>'priority','normal'),\n    COALESCE(p->>'subject','(no subject)'),\n    COALESCE(p->'body_json','{}'::jsonb) || '{\"attempt_count\": 0}'::jsonb,\n    NULLIF(p->>'related_lead_id','')::uuid\n  FROM input\n  WHERE $3 = 'memo'\n  RETURNING memo_id\n),\narchive_memo AS (\n  UPDATE bridge.agent_memos\n  SET status = 'resolved', resolved_at = NOW(), resolved_by = $2,\n      resolution_notes = COALESCE((SELECT p->>'reason' FROM input), 'archived by bot')\n  WHERE $3 = 'archive'\n    AND memo_id = (SELECT NULLIF((p->>'memo_id'),'')::uuid FROM input)\n  RETURNING memo_id\n),\nevent_log AS (\n  INSERT INTO bridge.workflow_events\n    (workflow_name, event_type, details_json)\n  SELECT\n    $2 || '_bot',\n    CASE $3\n      WHEN 'memo'    THEN 'memo_created'\n      WHEN 'archive' THEN 'memo_archived'\n      WHEN 'no_op'   THEN 'no_op'\n      ELSE 'other_low_risk'\n    END,\n    (SELECT p FROM input)\n  RETURNING event_id\n)\nSELECT\n  (SELECT memo_id FROM memo_insert)  AS memo_created,\n  (SELECT memo_id FROM archive_memo) AS memo_archived,\n  (SELECT event_id FROM event_log)   AS event_logged;\n",
        "options": {
          "queryReplacement": "={{ JSON.stringify($json.action_payload) }},{{ $json.bot_name }},{{ $json.action_type }}"
        }
      },
      "id": "node_exec_low_risk",
      "name": "Execute low-risk action",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2440,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.telegram.org/bot{{$env.BRIDGE_PM_BOT_TOKEN || $env.Bridge_CEO_BOT}}/sendMessage",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json; charset=utf-8"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"chat_id\": {{$env.BRIDGE_ADMIN_TELEGRAM_CHAT_ID}}, \"text\": \"\\u26a0\\ufe0f  *Approval request from {{$json.bot_name}}*\\n\\n  Action:  `{{$json.action_type}}`\\n  Risk:    medium\\n  Payload: {{ JSON.stringify($json.action_payload).replace(/\"/g, \"'\").slice(0,600) }}\\n\\nReply *APPROVE {{$json.bot_name}} {{$json.action_type}}* to proceed, or ignore to reject.\\n\\u2014 Bridge Agents\", \"parse_mode\": \"Markdown\" }",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node_medium_approval",
      "name": "Medium-risk: approval DM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2440,
        500
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO bridge.agent_memos (from_agent, to_agent, memo_type, priority, subject, body_json) VALUES ($1, 'chief_of_staff', 'approval_request', 'high',         'Pending medium-risk action awaiting user approval', $2::jsonb) RETURNING memo_id;",
        "options": {
          "queryReplacement": "={{ $json.bot_name }},{{ JSON.stringify({action_type: $json.action_type, action_payload: $json.action_payload}) }}"
        }
      },
      "id": "node_medium_memo",
      "name": "Medium-risk: log pending",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2660,
        500
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO bridge.agent_memos (from_agent, to_agent, memo_type, priority, subject, body_json) VALUES ('pm', 'ceo', 'escalation', 'urgent',         'HIGH-RISK action requires manual execution', $1::jsonb) RETURNING memo_id;",
        "options": {
          "queryReplacement": "={{ JSON.stringify({action_type: $json.action_type, action_payload: $json.action_payload}) }}"
        }
      },
      "id": "node_high_escalate",
      "name": "High-risk: escalate to CEO",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2440,
        700
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.telegram.org/bot{{$env.BRIDGE_PM_BOT_TOKEN || $env.Bridge_CEO_BOT}}/sendMessage",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json; charset=utf-8"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"chat_id\": {{$env.BRIDGE_ADMIN_TELEGRAM_CHAT_ID}}, \"text\": \"\\ud83d\\udea8  *HIGH-RISK action blocked (escalated to CEO)*\\n\\n  From:    {{$json.bot_name}}\\n  Action:  `{{$json.action_type}}`\\n  Payload: {{ JSON.stringify($json.action_payload).slice(0,600) }}\\n\\nThis action was NOT executed. Review the agent_memos table for the escalation.\\n\\u2014 Bridge Agents\", \"parse_mode\": \"Markdown\" }",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "node_high_alert",
      "name": "High-risk: urgent DM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2660,
        700
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"ok\": true, \"bot\": {{JSON.stringify($(\"Parse response + risk tag\").first().json.bot_name)}}}",
        "options": {}
      },
      "id": "node_respond",
      "name": "Respond OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        3320,
        400
      ]
    }
  ],
  "connections": {
    "Telegram: user DM": {
      "main": [
        [
          {
            "node": "Build task payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: daily": {
      "main": [
        [
          {
            "node": "Build task payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: weekly": {
      "main": [
        [
          {
            "node": "Build task payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook: invoke": {
      "main": [
        [
          {
            "node": "Build task payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build task payload": {
      "main": [
        [
          {
            "node": "Read enabled flag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read enabled flag": {
      "main": [
        [
          {
            "node": "If enabled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If enabled": {
      "main": [
        [
          {
            "node": "Fetch open inbox",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Fetch open inbox": {
      "main": [
        [
          {
            "node": "Fetch bot context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch bot context": {
      "main": [
        [
          {
            "node": "Assemble prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assemble prompt": {
      "main": [
        [
          {
            "node": "super-agent /webhook/bot-engine",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse response + risk tag": {
      "main": [
        [
          {
            "node": "If has reply",
            "type": "main",
            "index": 0
          },
          {
            "node": "Memory ingest",
            "type": "main",
            "index": 0
          },
          {
            "node": "Split actions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If has reply": {
      "main": [
        [
          {
            "node": "Reply on Telegram",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Reply on Telegram": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Memory ingest": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split actions": {
      "main": [
        [
          {
            "node": "Switch by risk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch by risk": {
      "main": [
        [
          {
            "node": "Execute low-risk action",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Medium-risk: approval DM",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "High-risk: escalate to CEO",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute low-risk action": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Medium-risk: approval DM": {
      "main": [
        [
          {
            "node": "Medium-risk: log pending",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Medium-risk: log pending": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "High-risk: escalate to CEO": {
      "main": [
        [
          {
            "node": "High-risk: urgent DM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "High-risk: urgent DM": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "super-agent /webhook/bot-engine": {
      "main": [
        [
          {
            "node": "Parse response + risk tag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}

Credentials you'll need

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

Pro

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

About this workflow

bridge_pm_bot. Uses telegramTrigger, postgres, httpRequest. Event-driven trigger; 23 nodes.

Source: https://github.com/gelson12/super-agent/blob/c6b7435c3ef54ed3f7432efdc42e0aed35990df8/n8n/bridge_pm_bot.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

Bridge_Finance_BOT. Uses telegramTrigger, postgres, httpRequest. Event-driven trigger; 23 nodes.

Telegram Trigger, Postgres, HTTP Request
AI & RAG

Gmail-Triage. Uses executeWorkflowTrigger, httpRequest, postgres. Event-driven trigger; 10 nodes.

Execute Workflow Trigger, HTTP Request, Postgres
AI & RAG

Phase 0 — Telegram Foundation. Uses telegramTrigger, telegram, httpRequest. Event-driven trigger; 9 nodes.

Telegram Trigger, Telegram, HTTP Request
AI & RAG

Lead Interaction (Supabase) - AutoDeployed. Uses telegramTrigger, httpRequest, telegram. Event-driven trigger; 7 nodes.

Telegram Trigger, HTTP Request, Telegram
AI & RAG

Model Onboarding (Supabase) - AutoDeployed. Uses telegramTrigger, httpRequest, telegram. Event-driven trigger; 6 nodes.

Telegram Trigger, HTTP Request, Telegram