{"slug": "building-a-human-in-the-loop-autonomous-coding-agent-with-n8n-and-telegram", "title": "Building a Human-in-the-Loop Autonomous Coding Agent with n8n and Telegram", "summary": "A developer built a human-in-the-loop autonomous coding agent that integrates Telegram, Cloudflare Tunnel, n8n, an Express.js runner, and Codex to enable remote coding with approval-based control. The system runs locally on the developer's machine, using Cloudflare Tunnel to bridge Telegram webhooks to the local n8n instance, and escalates sensitive actions to the developer's phone for approval. The architecture allows the agent to work independently on routine tasks while requiring human consent for operations outside granted permissions.", "body_md": "What if I could send a coding task from my phone, put the phone back in my pocket, and let an AI agent work on my development machine?\n\nAnd what if, whenever the agent wanted to do something that required my permission, it could simply message me on Telegram?\n\nThat was the idea behind a small automation project I recently built.\n\nThe final architecture combines **Telegram, Cloudflare Tunnel, n8n, a lightweight Express.js runner, and Codex** to create a remote, human-in-the-loop autonomous coding workflow.\n\nThe interesting part is that almost everything still runs on my own development machine.\n\nI didn't deploy n8n or my Codex runner to a public server just to make Telegram communication possible. Instead, I use **Cloudflare Tunnel** as the bridge between the public internet and my local n8n instance.\n\nThe result looks roughly like this:\n\n```\n                    INTERNET\n                        │\n                        ▼\n┌─────────────┐   ┌───────────────┐\n│  Telegram   │──▶│  Cloudflare   │\n│    Bot      │   │    Tunnel     │\n└─────────────┘   └───────┬───────┘\n                          │\n                          │ secure tunnel\n                          ▼\n                 ┌────────────────┐\n                 │   Local n8n    │\n                 └───────┬────────┘\n                         │\n                         ▼\n                 ┌────────────────┐\n                 │ Express Runner │\n                 └───────┬────────┘\n                         │\n                         ▼\n                 ┌────────────────┐\n                 │     Codex      │\n                 └───────┬────────┘\n                         │\n                         ▼\n                    Local Repo\n```\n\nAnd when Codex requires approval:\n\n```\nCodex\n  │\n  │ approval required\n  ▼\nExpress Runner\n  │\n  ▼\nn8n\n  │\n  ▼\nTelegram\n  │\n  │ Approve / Reject\n  ▼\nn8n\n  │\n  ▼\nExpress\n  │\n  ▼\nCodex continues\n```\n\nThis article explains how I built it and, more importantly, some of the architectural problems that appeared once I tried turning a coding agent into something I could actually operate remotely.\n\nAI coding agents are already capable of doing much more than generating code snippets.\n\nThey can inspect a repository, modify multiple files, execute shell commands, run tests, inspect failures, fix their implementation, and repeat the process.\n\nBut there was still one inconvenience in my workflow:\n\n**I had to be sitting at my development machine to initiate and supervise the work.**\n\nI wanted something closer to this:\n\n```\nMe, from Telegram:\n\n\"resume-web:\nAdd rate limiting to the API\nand add tests.\"\n\n        ↓\n\nCodex works on my PC\n\n        ↓\n\nTelegram:\n\n\"Task completed.\n12 tests passed.\"\n```\n\nAll from my phone.\n\nBut I had another requirement.\n\nI didn't want to achieve remote coding by simply giving an AI agent unrestricted control over my machine.\n\nIf the agent wants to perform something outside the permissions I've granted, I still want to make that decision.\n\nSo the real goal became:\n\nBuild an autonomous coding workflow where normal development work can proceed independently, while sensitive actions are escalated back to my phone for approval.\n\nI think **human-in-the-loop autonomous coding** is the best description for it.\n\nThe first question was how I wanted to interact with the system.\n\nI could have built a web dashboard.\n\nBut then I'd have to build authentication, a frontend, notifications, mobile responsiveness, task history, and several other things unrelated to the actual experiment.\n\nTelegram already gives me most of that.\n\nIt provides:\n\nMore importantly:\n\n**it's already on my phone.**\n\nSo Telegram became my remote control.\n\n```\nTelegram = Remote UI\n```\n\nThere was an immediate problem.\n\nMy n8n instance runs locally.\n\nSomething like:\n\n```\nhttp://localhost:5678\n```\n\nThat's perfectly fine when I'm sitting at my computer.\n\nBut Telegram obviously cannot send webhook requests to:\n\n```\nlocalhost:5678\n```\n\n`localhost`\n\nonly exists from the perspective of my machine.\n\nI needed:\n\n```\nPublic Internet\n      ↓\nPublic HTTPS endpoint\n      ↓\nMy local n8n\n```\n\nOne solution would have been deploying n8n somewhere publicly accessible.\n\nBut I specifically wanted to keep this automation running on my own machine.\n\nThat's where **Cloudflare Tunnel** entered the architecture.\n\nCloudflare Tunnel lets a locally running service become reachable through a public hostname without exposing the machine itself directly.\n\nInstead of opening a router port like:\n\n```\nInternet\n   ↓\nPublic IP\n   ↓\nPort forwarding\n   ↓\nMy PC\n```\n\nI run `cloudflared`\n\nlocally.\n\nThe connection is initiated **outbound** from my machine toward Cloudflare.\n\nConceptually:\n\n```\nMy PC\n │\n │ outbound connection\n ▼\nCloudflare\n │\n │ public HTTPS\n ▼\nInternet\n```\n\nThen Cloudflare can map a public hostname to my local n8n service.\n\nFor example:\n\n```\nautomation.example.com\n          │\n          ▼\n  Cloudflare Tunnel\n          │\n          ▼\nhttp://localhost:5678\n```\n\nThis means Telegram can reach a public HTTPS webhook while n8n itself continues running locally.\n\nSo my webhook path can conceptually become:\n\n```\nhttps://automation.example.com/webhook/telegram\n```\n\nwhile the actual service receiving it is still:\n\n```\nhttp://localhost:5678\n```\n\nI don't have to expose n8n's port directly to the internet.\n\nI didn't want my architecture to become:\n\n```\nTelegram\n   ↓\nMY_PUBLIC_IP:5678\n   ↓\nn8n\n```\n\nThat would require exposing an inbound service from my development network.\n\nCloudflare Tunnel gives me a cleaner model:\n\n```\n                    Public Internet\n                           │\n                           ▼\n                    ┌─────────────┐\n                    │ Cloudflare  │\n                    └──────┬──────┘\n                           │\n                     secure tunnel\n                           │\n                    ┌──────▼──────┐\n                    │ cloudflared │\n                    │   My PC     │\n                    └──────┬──────┘\n                           │\n                           ▼\n                    localhost:5678\n                           │\n                           ▼\n                          n8n\n```\n\nThe tunnel is initiated from the local machine rather than requiring inbound port forwarding.\n\nFor a personal automation project like this, that was exactly what I wanted.\n\nOne thing that's important to clarify is that Cloudflare isn't running my coding workflow.\n\nIt doesn't execute Codex.\n\nIt doesn't orchestrate the tasks.\n\nAnd it doesn't run n8n.\n\nIts job in this architecture is much smaller:\n\n```\nCloudflare Tunnel\n       =\nPublic ingress to local n8n\n```\n\nThe actual execution still happens locally:\n\n```\nLOCAL DEVELOPMENT MACHINE\n\ncloudflared\n     │\n     ▼\n    n8n\n     │\n     ▼\nExpress.js\n     │\n     ▼\nCodex\n     │\n     ▼\nRepository\n```\n\nI like this separation because each component has a very specific responsibility.\n\nOnce Telegram can reach my local environment, I still need something to orchestrate the workflow.\n\nFor example:\n\n```\nReceive Telegram message\n        ↓\nParse project + instruction\n        ↓\nValidate input\n        ↓\nCall Codex runner\n        ↓\nWait for completion\n        ↓\nFormat result\n        ↓\nSend Telegram response\n```\n\nThat's where n8n fits.\n\nMy simplified workflow looks something like:\n\n```\nTelegram Trigger\n       │\n       ▼\nParse Command\n       │\n       ▼\nHTTP Request\n       │\n       ▼\nLocal Codex Runner\n       │\n       ▼\nFormat Result\n       │\n       ▼\nTelegram Message\n```\n\nn8n isn't the coding agent.\n\nIt's the **orchestrator**.\n\nI still needed something that could actually launch and communicate with Codex.\n\nI built a lightweight Express.js service for that.\n\nConceptually, n8n calls:\n\n```\nPOST /codex\n```\n\nwith something like:\n\n```\n{\n  \"project\": \"resume-web\",\n  \"task\": \"Add rate limiting to the API and add tests.\"\n}\n```\n\nThe runner maps a project alias to an actual local directory:\n\n``` js\nconst projects = {\n  \"resume-web\": \"D:\\\\Projects\\\\resume-web\"\n};\n```\n\nThis was an important design decision.\n\nI don't allow Telegram to specify an arbitrary filesystem path.\n\nInstead of:\n\n```\n{\n  \"path\": \"C:\\\\whatever\\\\someone\\\\wants\"\n}\n```\n\nthe remote interface only knows:\n\n```\nresume-web\nbackend-api\npersonal-rag\n```\n\nThe runner decides where those repositories actually exist.\n\nSo:\n\n```\nTelegram\n   │\n   │ \"resume-web\"\n   ▼\nExpress\n   │\n   │ lookup\n   ▼\nD:\\Projects\\resume-web\n```\n\nThat gives me a simple allowlist around what the remote system can operate on.\n\nMy original runner was surprisingly simple.\n\nIt essentially spawned Codex:\n\n``` js\nconst child = spawn(\n  \"codex.cmd exec --sandbox workspace-write -\",\n  {\n    cwd: projectPath,\n    shell: true\n  }\n);\n```\n\nThen I collected stdout and stderr:\n\n``` js\nlet stdout = \"\";\nlet stderr = \"\";\n\nchild.stdout.on(\"data\", (data) => {\n  stdout += data.toString();\n});\n\nchild.stderr.on(\"data\", (data) => {\n  stderr += data.toString();\n});\n```\n\nFinally:\n\n``` js\nchild.on(\"close\", (code) => {\n  res.json({\n    success: code === 0,\n    output: stdout,\n    error: stderr\n  });\n});\n```\n\nThat produced:\n\n```\nTelegram\n    ↓\nCloudflare\n    ↓\nn8n\n    ↓\nExpress\n    ↓\ncodex exec\n    ↓\nRepository\n    ↓\nCodex exits\n    ↓\nExpress response\n    ↓\nn8n\n    ↓\nTelegram\n```\n\nAnd it worked.\n\nI could literally send a coding instruction from my phone and receive the Codex result back on Telegram.\n\nBut there was still one major problem.\n\nGiving an agent permission to edit code is one thing.\n\nGiving it unrestricted access to everything on the machine is another.\n\nI wanted Codex to work autonomously inside a controlled environment, but still ask me when it crossed a permission boundary.\n\nNormally this is easy when you're sitting in front of the CLI:\n\n```\nCodex wants to perform an action.\n\nApprove?\n\n[Y] Yes\n[N] No\n```\n\nBut my Codex process is being triggered remotely.\n\nThat created the most interesting problem in the entire project:\n\nHow can Codex pause a task, tell Express that it needs approval, send that request all the way to my phone, and then continue the exact same task after I approve it?\n\nMy first runner treated:\n\n```\nchild.on(\"close\", ...)\n```\n\nas:\n\n```\nTASK FINISHED\n```\n\nThat makes sense for a one-shot command.\n\nBut it becomes limiting once the agent needs an interactive lifecycle.\n\nWhat I actually needed was:\n\n```\nPersistent Codex process\n\n      │\n      ├── Turn\n      │    │\n      │    ├── working\n      │    ├── working\n      │    ├── approval required\n      │    │       ↓\n      │    │      WAIT\n      │    │       ↓\n      │    │    approved\n      │    │\n      │    ├── continue\n      │    │\n      │    └── completed\n      │\n      └── ready for next turn\n```\n\nThat led me to using **Codex App Server**.\n\nWith an event-driven Codex integration, Express can receive lifecycle messages while the task is still running.\n\nAn approval request is no longer equivalent to task completion.\n\nInstead:\n\n```\napproval request\n      ≠\ntask completed\n```\n\nCodex can effectively tell the runner:\n\n```\n\"I'm still working on this turn,\nbut I need a human decision before\nI can continue.\"\n```\n\nThe runner stores that pending approval and triggers a dedicated n8n webhook.\n\nThis is probably my favorite part of the system.\n\nSuppose Codex needs permission.\n\nThe request travels:\n\n```\nCodex\n   ↓\nExpress Runner\n   ↓\nn8n\n   ↓\nCloudflare Tunnel\n   ↓\nTelegram\n```\n\nOn my phone I receive something like:\n\n```\n⚠️ Codex Approval Required\n\nProject:\nresume-web\n\nAction:\n<requested operation>\n\n[ Approve ]   [ Reject ]\n```\n\nI press:\n\n```\nApprove\n```\n\nand the response travels back through the automation:\n\n```\nTelegram\n   ↓\nCloudflare\n   ↓\nn8n\n   ↓\nExpress Runner\n   ↓\nCodex\n```\n\nThe important part is that this isn't a new coding task.\n\nIt's a response to the **existing paused Codex turn**.\n\nCodex receives the decision and continues exactly where it stopped.\n\nThere was one design decision from my original implementation that I intentionally kept.\n\nThe n8n request waits until Codex finishes.\n\nWhy?\n\nBecause it gives me an extremely simple final-result pipeline:\n\n```\nCodex finishes\n      ↓\nExpress responds\n      ↓\nn8n immediately continues\n      ↓\nTelegram receives result\n```\n\nI don't need:\n\n```\nGET /task/status\nGET /task/status\nGET /task/status\nGET /task/status\n```\n\nor another polling mechanism.\n\nExpress simply awaits the Codex **turn**.\n\nConceptually:\n\n``` js\nconst result = await codexRunner.runTurn({\n  project,\n  task\n});\n\nres.json(result);\n```\n\nIf approval never happens:\n\n```\nTask\n ↓\nCodex\n ↓\nComplete\n ↓\nHTTP response\n```\n\nIf approval happens:\n\n```\nTask\n ↓\nCodex\n ↓\nApproval required\n ↓\nTelegram\n ↓\nApprove\n ↓\nCodex continues\n ↓\nComplete\n ↓\nHTTP response\n```\n\nFrom the original n8n workflow's perspective, both eventually produce the same result.\n\nI also wanted to know how long Codex actually takes to perform tasks.\n\nSo my runner logs execution duration.\n\nOriginally:\n\n```\n{\n  \"project\": \"resume-web\",\n  \"durationMs\": 128421,\n  \"success\": true\n}\n```\n\nBut remote approval introduced an interesting problem.\n\nSuppose:\n\n```\nCodex works         2 minutes\nI ignore Telegram   6 minutes\nCodex continues     1 minute\n```\n\nTotal wall-clock time:\n\n```\n9 minutes\n```\n\nBut saying:\n\n```\n\"Codex took 9 minutes\"\n```\n\nwouldn't really be accurate.\n\nSo I separated:\n\n```\n{\n  \"durationMs\": 540000,\n  \"codexActiveMs\": 180000,\n  \"approvalWaitMs\": 360000,\n  \"approvalCount\": 1\n}\n```\n\nThis opens up some interesting profiling possibilities later:\n\n```\nAverage task duration\nAverage Codex active time\nApproval frequency\nHuman response time\nFailure rate\nCommands per task\nPerformance by project\n```\n\nAfter putting everything together, my system now looks like this:\n\n```\n                         INTERNET\n\n                  ┌───────────────────┐\n                  │     Telegram      │\n                  │       Bot         │\n                  └─────────┬─────────┘\n                            │\n                            │ HTTPS\n                            ▼\n                  ┌───────────────────┐\n                  │    Cloudflare     │\n                  │      Tunnel       │\n                  └─────────┬─────────┘\n                            │\n                    outbound tunnel\n                            │\n════════════════════════════╪══════════════════\n                    LOCAL MACHINE\n                            │\n                            ▼\n                  ┌───────────────────┐\n                  │        n8n        │\n                  │   Orchestration   │\n                  └─────────┬─────────┘\n                            │\n                            │ localhost\n                            ▼\n                  ┌───────────────────┐\n                  │  Express Runner   │\n                  │                   │\n                  │ • project map     │\n                  │ • task lifecycle  │\n                  │ • approval state  │\n                  │ • profiling       │\n                  │ • logging         │\n                  └─────────┬─────────┘\n                            │\n                            │ JSON-RPC\n                            ▼\n                  ┌───────────────────┐\n                  │ Codex App Server  │\n                  │                   │\n                  │ • thread          │\n                  │ • turn            │\n                  │ • tools           │\n                  │ • approvals       │\n                  └─────────┬─────────┘\n                            │\n                            ▼\n                  ┌───────────────────┐\n                  │ Local Repository  │\n                  │                   │\n                  │ Code / Git / Test │\n                  └───────────────────┘\n```\n\nEach component has a very specific job:\n\n```\nTelegram\n    → Remote interface\n\nCloudflare Tunnel\n    → Public ingress to my local environment\n\nn8n\n    → Workflow orchestration\n\nExpress.js\n    → Codex process + task management\n\nCodex\n    → Autonomous coding agent\n\nGit repository\n    → Actual development workspace\n```\n\nI think that separation is one of the reasons the project stayed relatively understandable.\n\nI wouldn't describe this as fully autonomous software development.\n\nI'm still responsible for deciding what gets built, reviewing important changes, controlling permissions, and deciding what eventually reaches production.\n\nBut once I provide a task, the implementation loop can happen independently:\n\n```\nInspect\n   ↓\nPlan\n   ↓\nModify\n   ↓\nBuild\n   ↓\nTest\n   ↓\nFail\n   ↓\nDebug\n   ↓\nFix\n   ↓\nRetest\n   ↓\nComplete\n```\n\nAnd if the agent reaches something requiring my judgment:\n\n```\nAgent\n  ↓\nHuman decision required\n  ↓\nTelegram\n  ↓\nMe\n```\n\nThat's why I prefer the term:\n\nHuman-in-the-loop autonomous coding.\n\nThere is obviously a much easier solution to the approval problem:\n\nGive the coding agent unrestricted access.\n\nThen there are no approval interruptions.\n\nBut that's exactly what I didn't want.\n\nInstead, the philosophy behind my setup is:\n\n```\nNormal development work\n        ↓\n     AUTOMATIC\n\nSensitive operation\n        ↓\n    ASK HUMAN\n```\n\nFor example:\n\n```\nRead repository\n        → automatic\n\nEdit workspace\n        → automatic\n\nRun tests\n        → automatic\n\nInspect Git\n        → automatic\n\nSensitive command\n        → approval\n\nOutside workspace\n        → approval\n\nNetwork operation\n        → approval\n\nDestructive operation\n        → approval\n```\n\nThe goal isn't to remove myself from development.\n\nIt's to move myself:\n\n```\nFROM\n\nexecution loop\n\nTO\n\ndecision loop\n```\n\nAnd I think that's a much more useful form of automation.\n\nThe current system already works, but there are several things I'd like to improve.\n\nI want commands such as:\n\n```\n/status\n```\n\nto return something like:\n\n```\nProject: resume-web\nTask: Add API rate limiting\nStatus: Running\nDuration: 02:41\nApprovals: 0\n```\n\nI'd also like:\n\n```\n/cancel\n```\n\nto interrupt an active Codex turn.\n\nAnother obvious improvement is task queues.\n\nI don't necessarily want:\n\n```\nTask A ─┐\nTask B ─┼─→ same repository\nTask C ─┘\n```\n\nall editing files simultaneously.\n\nInstead:\n\n```\nresume-web\n\nTask A → RUNNING\nTask B → QUEUED\nTask C → QUEUED\n```\n\nGit branch isolation would also make the system much safer:\n\n```\nTelegram task\n      ↓\nCreate branch\n      ↓\nCodex works\n      ↓\nTests\n      ↓\nReturn diff\n      ↓\nHuman review\n      ↓\nMerge\n```\n\nAnd eventually, persistent state would help recover from crashes or restarts while approvals are pending.\n\nAt first, I thought this project would mostly be about AI.\n\nIt wasn't.\n\nThe most interesting problems ended up being orchestration problems:\n\n```\nHow does Telegram reach localhost?\n\nWho owns task state?\n\nWhat does \"completed\" mean?\n\nHow does an agent pause?\n\nHow do approvals travel between systems?\n\nHow does execution resume?\n\nWhat happens if something crashes?\n\nWhich operations should be automatic?\n\nWhich operations require humans?\n```\n\nThe model is only one part of an autonomous system.\n\nThe infrastructure around the model determines whether it can actually operate reliably.\n\nBefore building this, my workflow looked roughly like:\n\n```\nOpen laptop\n   ↓\nOpen terminal\n   ↓\nNavigate to repository\n   ↓\nLaunch coding agent\n   ↓\nGive instruction\n   ↓\nWatch execution\n   ↓\nApprove action\n   ↓\nWait\n   ↓\nReview\n```\n\nNow:\n\n```\nTake phone\n   ↓\nOpen Telegram\n   ↓\nSend task\n   ↓\nPut phone away\n```\n\nThen, if necessary:\n\n```\nTelegram:\n\n⚠️ Codex needs approval\n\n[Approve] [Reject]\n```\n\nAnd eventually:\n\n```\nTelegram:\n\n✅ Task completed\n```\n\nMy development machine is still doing the actual work.\n\nI'm still responsible for the important decisions.\n\nBut I no longer need to sit in front of it while the agent performs every step.\n\nAnd that's probably the biggest thing I learned from this project:\n\nThe next step for coding agents isn't necessarily removing developers. It's reducing how much of the execution loop requires a developer's continuous attention.\n\nSometimes the biggest improvement isn't another model.\n\nIt's building better infrastructure around the one you already have.", "url": "https://wpnews.pro/news/building-a-human-in-the-loop-autonomous-coding-agent-with-n8n-and-telegram", "canonical_source": "https://dev.to/anggbchtr/building-a-human-in-the-loop-autonomous-coding-agent-with-n8n-and-telegram-46gp", "published_at": "2026-08-19 07:37:50+00:00", "updated_at": "2026-08-19 07:41:32.150004+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-products"], "entities": ["Telegram", "Cloudflare Tunnel", "n8n", "Express.js", "Codex"], "alternates": {"html": "https://wpnews.pro/news/building-a-human-in-the-loop-autonomous-coding-agent-with-n8n-and-telegram", "markdown": "https://wpnews.pro/news/building-a-human-in-the-loop-autonomous-coding-agent-with-n8n-and-telegram.md", "text": "https://wpnews.pro/news/building-a-human-in-the-loop-autonomous-coding-agent-with-n8n-and-telegram.txt", "jsonld": "https://wpnews.pro/news/building-a-human-in-the-loop-autonomous-coding-agent-with-n8n-and-telegram.jsonld"}}