AutomationFlowsAI & RAG › Wf-00 Agent_router

Wf-00 Agent_router

WF-00 agent_router. Uses postgres, httpRequest, agent, lmChatOpenRouter. Scheduled trigger; 23 nodes.

Cron / scheduled trigger★★★★☆ complexityAI-powered23 nodesPostgresHTTP RequestAgentOpenRouter ChatMemory Postgres ChatTool WorkflowTelegram
AI & RAG Trigger: Cron / scheduled Nodes: 23 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the Agent → HTTP Request 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": "WF-00 agent_router",
  "nodes": [
    {
      "id": "poll",
      "name": "Poll updates",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "seconds",
              "secondsInterval": 20
            }
          ]
        }
      }
    },
    {
      "id": "offset",
      "name": "Get offset",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        180,
        0
      ],
      "parameters": {
        "operation": "executeQuery",
        "query": "select coalesce(max(update_id), 0) + 1 as tg_offset from tg_updates",
        "options": {}
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "executeOnce": true,
      "alwaysOutputData": true
    },
    {
      "id": "fetch",
      "name": "Fetch updates",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        360,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://api.telegram.org/bot__TG_TOKEN__/getUpdates?offset={{ $json.tg_offset || 1 }}&timeout=0",
        "options": {
          "timeout": 10000
        }
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "extract",
      "name": "Extract messages",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        540,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const j = $input.first().json;\nif (!j.ok || !Array.isArray(j.result)) return [];\nconst out = [];\nfor (const u of j.result) {\n  const m = u.message || u.edited_message || {};\n  out.push({ json: {\n    update_id: u.update_id,\n    chat_id: (m.chat && m.chat.id) || 0,\n    text: (m.text || '').slice(0, 4000)\n  } });\n}\nreturn out;"
      }
    },
    {
      "id": "record",
      "name": "Record update",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        720,
        0
      ],
      "parameters": {
        "operation": "executeQuery",
        "query": "insert into tg_updates (update_id, chat_id, text) values ($1::bigint, $2::bigint, $3) on conflict (update_id) do nothing returning update_id, chat_id, text",
        "options": {
          "queryReplacement": "={{ $json.update_id }},{{ $json.chat_id }},{{ $json.text }}"
        }
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "allow",
      "name": "Allowlisted?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        900,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "a1",
              "leftValue": "={{ String($json.chat_id) }}",
              "rightValue": "__TG_CHAT__",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "a2",
              "leftValue": "={{ ($json.text || \"\").length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        }
      }
    },
    {
      "id": "testwh",
      "name": "Test webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        720,
        200
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "overseer-router-test-__ZZ_SECRET__",
        "responseMode": "lastNode",
        "options": {}
      }
    },
    {
      "id": "testnorm",
      "name": "Test normalize",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        200
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const b = $input.first().json.body || {};\nreturn [{ json: { chat_id: Number(b.chat_id) || __TG_CHAT__, text: String(b.text || ''), test: true } }];"
      }
    },
    {
      "id": "iscmd",
      "name": "Feed switch?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1060,
        100
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "k1",
              "leftValue": "={{ ($json.text || '').trim().toLowerCase().replace(/[.!]+$/, '') }}",
              "rightValue": "^(fix|stop) feed$",
              "operator": {
                "type": "string",
                "operation": "regex"
              }
            }
          ]
        }
      }
    },
    {
      "id": "toggle",
      "name": "Toggle drain",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        1240,
        -60
      ],
      "parameters": {
        "operation": "executeQuery",
        "query": "with t as (insert into settings (key, value, updated_at) values ('drain_enabled', $1, now()) on conflict (key) do update set value = excluded.value, updated_at = now() returning value), m as (insert into notices (kind, last_sent) select 'feed_backlog', now() where $1 = 'false' on conflict (kind) do update set last_sent = now()) select (select value from t) as value, (select count(*) from (select rd.id from raw_docs rd left join feed_items f on f.raw_doc_id = rd.id group by rd.id having count(f.id) <> 1) a) as backlog_total",
        "options": {
          "queryReplacement": "={{ ($json.text || '').trim().toLowerCase().startsWith('fix') ? 'true' : 'false' }}"
        }
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "switchreply",
      "name": "Switch reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1420,
        -60
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const r = $input.first().json;\nconst n = Number(r.backlog_total) || 0;\nconst on = String(r.value) === 'true';\nlet text;\nif (on && n === 0) text = \"Nothing to fix \u2014 every saved page already has a summary.\";\nelse if (on) text = \"On it. Working through \" + n + \" page\" + (n === 1 ? '' : 's') +\n  \", about \" + Math.max(1, Math.round(n * 20 / 60)) + \" min while your PC stays on. Say \\\"stop feed\\\" to stop.\";\nelse text = \"Stopped. I'll leave it alone \u2014 say \\\"fix feed\\\" whenever you want.\";\nlet chat_id = __TG_CHAT__;\ntry { chat_id = $('Record update').first().json.chat_id || chat_id; } catch (e) {\n  try { chat_id = $('Test normalize').first().json.chat_id || chat_id; } catch (e2) {}\n}\nreturn [{ json: { chat_id, output: text } }];"
      }
    },
    {
      "id": "agent",
      "name": "Overseer Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        1120,
        100
      ],
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.text }}",
        "options": {
          "systemMessage": "=You are Research Overseer's dispatcher on Telegram. Route requests to tools; never do research yourself; never fabricate results or data \u2014 if a tool returns nothing, say so.\n\nRules:\n1. Free and instant actions run immediately, no confirmation: a pasted URL -> scrape_url; status questions -> run_status; reading existing findings -> query_feed; 'anything new today' -> run_discovery with source rss. These touch nothing that costs money.\n2. Anything that starts a web search costs money and about six minutes, so it needs the user's word first. Run it immediately ONLY if they used an explicit search verb (search, find me, look up, go get, dig into). If they merely asked a question \u2014 'what's happening with X', 'anything on X', 'how is X going' \u2014 reply with a one-line proposal naming the source and query, then STOP and wait. Execute on their next message if they say yes. full_sweep always needs confirmation.\n3. Retrospective questions are ALWAYS query_feed or run_status, and this OVERRIDES everything else even when a topic is named. Retrospective phrasing includes: catch me up, what did you find, what's new, anything good, show me, what did I miss, brief me, recap. \"Catch me up on funding this week\" is query_feed with max_age_days 7 \u2014 NOT a search, and NOT a confirmation prompt either; just read the feed and answer.\n3b. Before reporting that little or nothing was found, consider whether your own filters caused it: a near-empty result after you passed a tag is far more likely a bad filter than an empty feed. Retry query_feed once without the tag before telling the user there is nothing \u2014 and never answer a thin result by starting a search instead. If the question names a time ('today', 'last night', 'this week'), compute the ISO timestamp for the start of that window from the current time below and pass it to query_feed as the since parameter. Never answer a time-scoped question from an unfiltered read.\n4. When uncertain between search sources, fail WIDE: prefer full_sweep (with confirmation) or brave. Never guess narrow.\n4b. Call at most ONE discovery tool per user request, ever. After full_sweep or run_discovery returns, summarize what it returned and stop. If it found little, say so \u2014 do NOT run it again with different wording. Each run costs money and minutes.\n5. manage_schedule: parse natural language into cron, timezone Asia/Dhaka (UTC+6). 'every day at 8am' -> cron '0 8 * * *' via action create_recurring. Relative one-times ('in 3 hours') -> action create_onetime with an ISO timestamp you compute from the current time below. If phrasing is ambiguous ('tomorrow morning'), ask ONE clarifying question. Always echo the parsed schedule back and get a yes before creating. Other actions: cancel (needs id), list.\n6. manage_sources actions: add (url, optional label), remove (id or url), list.\n7. Reply tersely \u2014 this is Telegram. Plain text, no markdown formatting.\n\nCurrent time: {{ $now.setZone('Asia/Dhaka').toFormat('cccc yyyy-MM-dd HH:mm') }} (Asia/Dhaka, UTC+6). This is the local date and time \u2014 use it directly, do not convert it.",
          "maxIterations": 6
        }
      }
    },
    {
      "id": "model",
      "name": "OpenRouter Haiku",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        1000,
        320
      ],
      "parameters": {
        "model": "anthropic/claude-haiku-4.5",
        "options": {
          "temperature": 0
        }
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "memory",
      "name": "Chat memory",
      "type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
      "typeVersion": 1.3,
      "position": [
        1160,
        320
      ],
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $json.chat_id }}",
        "contextWindowLength": 10
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "tool_run_discovery",
      "name": "run_discovery",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.2,
      "position": [
        1300,
        320
      ],
      "parameters": {
        "description": "Run ONE search source now. COSTS MONEY AND ~6 MINUTES. Call it immediately ONLY when the user used an explicit search verb \u2014 search, find me, look up, go get, dig into, run a search. For anything phrased as a question ('what's happening with X', 'anything on X', 'how is X going') you MUST reply with a one-line proposal and STOP, without calling this tool. Call it on their next message only if they said yes. Asking costs one line; guessing wrong costs six minutes of their machine. Params: source (exa|tavily|brave|rss), query. exa = conceptual or thematic digs. tavily = a specific named topic. brave = broad general sweep. rss = recency from known feeds ('anything new today', cheap, no confirmation needed). Does NOT read past results.",
        "workflowId": {
          "__rl": true,
          "value": "ccdGJn47aX7PHBti",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "source": "={{ $fromAI('source', 'one of exa, tavily, brave, rss', 'string', '') }}",
            "query": "={{ $fromAI('query', 'the search query (empty for rss)', 'string', '') }}",
            "trigger": "agent"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "source",
              "displayName": "source",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "query",
              "displayName": "query",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "trigger",
              "displayName": "trigger",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "tool_full_sweep",
      "name": "full_sweep",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.2,
      "position": [
        1440,
        320
      ],
      "parameters": {
        "description": "Run ALL discovery sources (exa, tavily, brave, rss). Only for 'sweep', 'go wide', 'full run'. Expensive \u2014 always confirm with the user first. Only call after the user has confirmed.",
        "workflowId": {
          "__rl": true,
          "value": "du4KagpWdi5AT84v",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "query": "={{ $fromAI('query', 'optional focus query; empty string runs the default sweep', 'string', '') }}",
            "trigger": "agent"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "query",
              "displayName": "query",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "trigger",
              "displayName": "trigger",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "tool_scrape_url",
      "name": "scrape_url",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.2,
      "position": [
        1580,
        320
      ],
      "parameters": {
        "description": "Ingest one URL the user pasted. Use whenever the message contains a link. Cheap \u2014 execute immediately, no confirmation.",
        "workflowId": {
          "__rl": true,
          "value": "fzgIhUYkPOaSdCUu",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "url": "={{ $fromAI('url', 'the full URL to scrape', 'string', '') }}",
            "trigger": "agent"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "url",
              "displayName": "url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "trigger",
              "displayName": "trigger",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "tool_query_feed",
      "name": "query_feed",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.2,
      "position": [
        1720,
        320
      ],
      "parameters": {
        "description": "Read EXISTING triaged results. Use for 'what did you find', 'show me', 'anything good last night'. NEVER triggers a new run. CRITICAL: if the question implies ANY time window \u2014 today, tonight, last night, this morning, yesterday, this week, recently, just now \u2014 you MUST pass `since` as an ISO timestamp you compute from the current time given above. Omitting it returns the whole archive, and you will report old items as if they were new. Only omit `since` for questions with no time element at all ('show me the best stuff', 'anything on agents'). Items are scored on two axes: specificity (1-5, how concrete and verifiable the claims are \u2014 vendor SEO content scores low) and angle_strength (1-5, how non-obvious the publishable hook is). score = the two added, 2-10. Use min_score 7+ for 'the good stuff', 8+ for 'only the best'. Quality and recency are separate knobs and combine freely: min_score filters how good, max_age_days filters how fresh. 'anything good this week' = min_score 7 + max_age_days 7. Results always come back best-first. Set group_stories to 'true' when the user wants an overview rather than every article \u2014 it collapses multiple outlets covering the same event into one row with an articles count. Prefer it for 'what's happening with X', 'catch me up', 'what did I miss'. Leave empty when they want individual pieces to read. DO NOT invent a tag. Leave tag empty unless the user names a topic you have actually seen in earlier results \u2014 a guessed tag silently filters almost everything out and makes an empty feed look like an empty world. Time words and quality belong in max_age_days and min_score, never in tag. Params: max_age_days (days since publication), min_score (2-10), since (ISO timestamp, for an exact cutoff), tag, limit (default 20), group_stories, min_relevance (legacy, leave empty).",
        "workflowId": {
          "__rl": true,
          "value": "VaA4YM6N4ni7F6CB",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "group_stories": "={{ $fromAI('group_stories', '\\'true\\' to collapse duplicate coverage of one event into a single row, empty for every article', 'string', '') }}",
            "max_age_days": "={{ $fromAI('max_age_days', 'only items published within this many days, empty for any age', 'string', '') }}",
            "min_score": "={{ $fromAI('min_score', 'minimum combined score 2-10, empty for none', 'string', '') }}",
            "since": "={{ $fromAI('since', 'ISO timestamp lower bound, empty for none', 'string', '') }}",
            "tag": "={{ $fromAI('tag', 'leave empty unless the user named a topic you have seen in the feed; partial matches work', 'string', '') }}",
            "limit": "={{ $fromAI('limit', 'max items to return, empty for default 20', 'string', '') }}",
            "min_relevance": "={{ $fromAI('min_relevance', 'legacy 1-5 filter, leave empty', 'string', '') }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "group_stories",
              "displayName": "group_stories",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "max_age_days",
              "displayName": "max_age_days",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "min_score",
              "displayName": "min_score",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "since",
              "displayName": "since",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "tag",
              "displayName": "tag",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "limit",
              "displayName": "limit",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "min_relevance",
              "displayName": "min_relevance",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "tool_run_status",
      "name": "run_status",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.2,
      "position": [
        1860,
        320
      ],
      "parameters": {
        "description": "Report recent runs: when, scope, counts, ok/partial/failed. Use for 'did last night's run work', 'when did you last run', 'did anything break'.",
        "workflowId": {
          "__rl": true,
          "value": "Ni3bddpEPN91ISg2",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "limit": "={{ $fromAI('limit', 'how many recent runs to return, empty for default 5', 'string', '') }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "limit",
              "displayName": "limit",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "tool_manage_schedule",
      "name": "manage_schedule",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.2,
      "position": [
        2000,
        320
      ],
      "parameters": {
        "description": "Create, cancel or list schedules. Params: action (create_recurring|create_onetime|cancel|list); cron (for create_recurring, 5-field, Asia/Dhaka); run_at (for create_onetime, ISO timestamp); label; id (for cancel). Echo the parsed schedule back for confirmation BEFORE calling create.",
        "workflowId": {
          "__rl": true,
          "value": "GiJSUs1kqil1jtsp",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "action": "={{ $fromAI('action', 'create_recurring, create_onetime, cancel, or list', 'string', '') }}",
            "cron": "={{ $fromAI('cron', '5-field cron expression for create_recurring, else empty', 'string', '') }}",
            "run_at": "={{ $fromAI('run_at', 'ISO timestamp for create_onetime, else empty', 'string', '') }}",
            "label": "={{ $fromAI('label', 'short human label for the schedule, optional', 'string', '') }}",
            "id": "={{ $fromAI('id', 'schedule id for cancel, else empty', 'string', '') }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "action",
              "displayName": "action",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "cron",
              "displayName": "cron",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "run_at",
              "displayName": "run_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "label",
              "displayName": "label",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "tool_manage_sources",
      "name": "manage_sources",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.2,
      "position": [
        2140,
        320
      ],
      "parameters": {
        "description": "Add, remove or list RSS feeds. Use for 'add this feed', 'stop following X', 'what feeds are you watching'. Params: action (add|remove|list); url (for add/remove); label (optional); id (for remove).",
        "workflowId": {
          "__rl": true,
          "value": "qAGxheHskgw3Ki9A",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "action": "={{ $fromAI('action', 'add, remove, or list', 'string', '') }}",
            "url": "={{ $fromAI('url', 'feed URL for add or remove, else empty', 'string', '') }}",
            "label": "={{ $fromAI('label', 'short label for the feed, optional', 'string', '') }}",
            "id": "={{ $fromAI('id', 'source id for remove, else empty', 'string', '') }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "action",
              "displayName": "action",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "url",
              "displayName": "url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "label",
              "displayName": "label",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            },
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true,
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "prepreply",
      "name": "Prep reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1400,
        100
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Telegram message preparation. Inlined into WF-00's Prep reply node by\n// scripts/build-wf00.js, so tests/telegram-format.test.js covers what ships.\n//\n// Messages are sent with parse_mode HTML because the node's legacy Markdown\n// default breaks on a single stray asterisk. That means two obligations:\n// escape HTML-significant characters, and strip the markdown the model emits\n// anyway despite being told not to (it would otherwise show as literal * and _).\n\nfunction escapeHtml(s) {\n  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nfunction stripMarkdown(s) {\n  return String(s)\n    .replace(/\\*\\*(.+?)\\*\\*/g, '$1')\n    .replace(/(^|\\s)\\*(?!\\s)(.+?)(?<!\\s)\\*(?=\\s|$)/g, '$1$2')\n    .replace(/(^|\\s)_(?!\\s)(.+?)(?<!\\s)_(?=\\s|$)/g, '$1$2')\n    .replace(/^\\s{0,3}#{1,6}\\s+/gm, '');\n}\n\n// What actually goes on the wire.\nfunction forTelegram(s, maxLen = 4000) {\n  return escapeHtml(stripMarkdown(String(s == null ? '' : s).slice(0, maxLen)));\n}\n\nlet chat_id = __TG_CHAT__;\ntry { chat_id = $('Record update').item.json.chat_id || chat_id; } catch (e) {\n  try { chat_id = $('Test normalize').item.json.chat_id || chat_id; } catch (e2) {}\n}\nreturn { json: { chat_id, output: forTelegram($json.output) || '(empty reply)' } };"
      }
    },
    {
      "id": "reply",
      "name": "Send reply",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1600,
        100
      ],
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "={{ $json.output }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    }
  ],
  "connections": {
    "Poll updates": {
      "main": [
        [
          {
            "node": "Get offset",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get offset": {
      "main": [
        [
          {
            "node": "Fetch updates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch updates": {
      "main": [
        [
          {
            "node": "Extract messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract messages": {
      "main": [
        [
          {
            "node": "Record update",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Record update": {
      "main": [
        [
          {
            "node": "Allowlisted?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Allowlisted?": {
      "main": [
        [
          {
            "node": "Feed switch?",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Test webhook": {
      "main": [
        [
          {
            "node": "Test normalize",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test normalize": {
      "main": [
        [
          {
            "node": "Feed switch?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Feed switch?": {
      "main": [
        [
          {
            "node": "Toggle drain",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Overseer Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Toggle drain": {
      "main": [
        [
          {
            "node": "Switch reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch reply": {
      "main": [
        [
          {
            "node": "Send reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Haiku": {
      "ai_languageModel": [
        [
          {
            "node": "Overseer Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Chat memory": {
      "ai_memory": [
        [
          {
            "node": "Overseer Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "run_discovery": {
      "ai_tool": [
        [
          {
            "node": "Overseer Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "full_sweep": {
      "ai_tool": [
        [
          {
            "node": "Overseer Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "scrape_url": {
      "ai_tool": [
        [
          {
            "node": "Overseer Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "query_feed": {
      "ai_tool": [
        [
          {
            "node": "Overseer Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "run_status": {
      "ai_tool": [
        [
          {
            "node": "Overseer Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "manage_schedule": {
      "ai_tool": [
        [
          {
            "node": "Overseer Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "manage_sources": {
      "ai_tool": [
        [
          {
            "node": "Overseer Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Overseer Agent": {
      "main": [
        [
          {
            "node": "Prep reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep reply": {
      "main": [
        [
          {
            "node": "Send reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "errorWorkflow": "PNJMA4NbQGmp1xKv"
  }
}

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

WF-00 agent_router. Uses postgres, httpRequest, agent, lmChatOpenRouter. Scheduled trigger; 23 nodes.

Source: https://github.com/killerwaz/research-overseer/blob/master/workflows/wf00-agent-router.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

System Architecture Two integrated N8N workflows providing automated US stock portfolio management through Telegram:

Output Parser Autofixing, OpenAI Chat, Perplexity +10
AI & RAG

Brokeria-v20. Uses n8n-nodes-waha, httpRequest, redis, googleGemini. Webhook trigger; 56 nodes.

N8N Nodes Waha, HTTP Request, Redis +7
AI & RAG

Brokeria-v15. Uses n8n-nodes-waha, httpRequest, postgres, redis. Webhook trigger; 55 nodes.

N8N Nodes Waha, HTTP Request, Postgres +7
AI & RAG

This workflow automatically generates stock market insights for selected tickers (e.g. GAZP, SBER, LKOH) using historical data, technical indicators, and an AI model. The results are then sent to Tele

Agent, OpenRouter Chat, Telegram Trigger +5
AI & RAG

AI Database Assistant with Smart Query's & PostgreSQL Integration

Execute Workflow Trigger, Agent, Tool Think +7