Building a Human-in-the-Loop Autonomous Coding Agent with n8n and Telegram 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. 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? And what if, whenever the agent wanted to do something that required my permission, it could simply message me on Telegram? That was the idea behind a small automation project I recently built. The 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. The interesting part is that almost everything still runs on my own development machine. I 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. The result looks roughly like this: INTERNET │ ▼ ┌─────────────┐ ┌───────────────┐ │ Telegram │──▶│ Cloudflare │ │ Bot │ │ Tunnel │ └─────────────┘ └───────┬───────┘ │ │ secure tunnel ▼ ┌────────────────┐ │ Local n8n │ └───────┬────────┘ │ ▼ ┌────────────────┐ │ Express Runner │ └───────┬────────┘ │ ▼ ┌────────────────┐ │ Codex │ └───────┬────────┘ │ ▼ Local Repo And when Codex requires approval: Codex │ │ approval required ▼ Express Runner │ ▼ n8n │ ▼ Telegram │ │ Approve / Reject ▼ n8n │ ▼ Express │ ▼ Codex continues This 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. AI coding agents are already capable of doing much more than generating code snippets. They can inspect a repository, modify multiple files, execute shell commands, run tests, inspect failures, fix their implementation, and repeat the process. But there was still one inconvenience in my workflow: I had to be sitting at my development machine to initiate and supervise the work. I wanted something closer to this: Me, from Telegram: "resume-web: Add rate limiting to the API and add tests." ↓ Codex works on my PC ↓ Telegram: "Task completed. 12 tests passed." All from my phone. But I had another requirement. I didn't want to achieve remote coding by simply giving an AI agent unrestricted control over my machine. If the agent wants to perform something outside the permissions I've granted, I still want to make that decision. So the real goal became: Build an autonomous coding workflow where normal development work can proceed independently, while sensitive actions are escalated back to my phone for approval. I think human-in-the-loop autonomous coding is the best description for it. The first question was how I wanted to interact with the system. I could have built a web dashboard. But then I'd have to build authentication, a frontend, notifications, mobile responsiveness, task history, and several other things unrelated to the actual experiment. Telegram already gives me most of that. It provides: More importantly: it's already on my phone. So Telegram became my remote control. Telegram = Remote UI There was an immediate problem. My n8n instance runs locally. Something like: http://localhost:5678 That's perfectly fine when I'm sitting at my computer. But Telegram obviously cannot send webhook requests to: localhost:5678 localhost only exists from the perspective of my machine. I needed: Public Internet ↓ Public HTTPS endpoint ↓ My local n8n One solution would have been deploying n8n somewhere publicly accessible. But I specifically wanted to keep this automation running on my own machine. That's where Cloudflare Tunnel entered the architecture. Cloudflare Tunnel lets a locally running service become reachable through a public hostname without exposing the machine itself directly. Instead of opening a router port like: Internet ↓ Public IP ↓ Port forwarding ↓ My PC I run cloudflared locally. The connection is initiated outbound from my machine toward Cloudflare. Conceptually: My PC │ │ outbound connection ▼ Cloudflare │ │ public HTTPS ▼ Internet Then Cloudflare can map a public hostname to my local n8n service. For example: automation.example.com │ ▼ Cloudflare Tunnel │ ▼ http://localhost:5678 This means Telegram can reach a public HTTPS webhook while n8n itself continues running locally. So my webhook path can conceptually become: https://automation.example.com/webhook/telegram while the actual service receiving it is still: http://localhost:5678 I don't have to expose n8n's port directly to the internet. I didn't want my architecture to become: Telegram ↓ MY PUBLIC IP:5678 ↓ n8n That would require exposing an inbound service from my development network. Cloudflare Tunnel gives me a cleaner model: Public Internet │ ▼ ┌─────────────┐ │ Cloudflare │ └──────┬──────┘ │ secure tunnel │ ┌──────▼──────┐ │ cloudflared │ │ My PC │ └──────┬──────┘ │ ▼ localhost:5678 │ ▼ n8n The tunnel is initiated from the local machine rather than requiring inbound port forwarding. For a personal automation project like this, that was exactly what I wanted. One thing that's important to clarify is that Cloudflare isn't running my coding workflow. It doesn't execute Codex. It doesn't orchestrate the tasks. And it doesn't run n8n. Its job in this architecture is much smaller: Cloudflare Tunnel = Public ingress to local n8n The actual execution still happens locally: LOCAL DEVELOPMENT MACHINE cloudflared │ ▼ n8n │ ▼ Express.js │ ▼ Codex │ ▼ Repository I like this separation because each component has a very specific responsibility. Once Telegram can reach my local environment, I still need something to orchestrate the workflow. For example: Receive Telegram message ↓ Parse project + instruction ↓ Validate input ↓ Call Codex runner ↓ Wait for completion ↓ Format result ↓ Send Telegram response That's where n8n fits. My simplified workflow looks something like: Telegram Trigger │ ▼ Parse Command │ ▼ HTTP Request │ ▼ Local Codex Runner │ ▼ Format Result │ ▼ Telegram Message n8n isn't the coding agent. It's the orchestrator . I still needed something that could actually launch and communicate with Codex. I built a lightweight Express.js service for that. Conceptually, n8n calls: POST /codex with something like: { "project": "resume-web", "task": "Add rate limiting to the API and add tests." } The runner maps a project alias to an actual local directory: js const projects = { "resume-web": "D:\\Projects\\resume-web" }; This was an important design decision. I don't allow Telegram to specify an arbitrary filesystem path. Instead of: { "path": "C:\\whatever\\someone\\wants" } the remote interface only knows: resume-web backend-api personal-rag The runner decides where those repositories actually exist. So: Telegram │ │ "resume-web" ▼ Express │ │ lookup ▼ D:\Projects\resume-web That gives me a simple allowlist around what the remote system can operate on. My original runner was surprisingly simple. It essentially spawned Codex: js const child = spawn "codex.cmd exec --sandbox workspace-write -", { cwd: projectPath, shell: true } ; Then I collected stdout and stderr: js let stdout = ""; let stderr = ""; child.stdout.on "data", data = { stdout += data.toString ; } ; child.stderr.on "data", data = { stderr += data.toString ; } ; Finally: js child.on "close", code = { res.json { success: code === 0, output: stdout, error: stderr } ; } ; That produced: Telegram ↓ Cloudflare ↓ n8n ↓ Express ↓ codex exec ↓ Repository ↓ Codex exits ↓ Express response ↓ n8n ↓ Telegram And it worked. I could literally send a coding instruction from my phone and receive the Codex result back on Telegram. But there was still one major problem. Giving an agent permission to edit code is one thing. Giving it unrestricted access to everything on the machine is another. I wanted Codex to work autonomously inside a controlled environment, but still ask me when it crossed a permission boundary. Normally this is easy when you're sitting in front of the CLI: Codex wants to perform an action. Approve? Y Yes N No But my Codex process is being triggered remotely. That created the most interesting problem in the entire project: How 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? My first runner treated: child.on "close", ... as: TASK FINISHED That makes sense for a one-shot command. But it becomes limiting once the agent needs an interactive lifecycle. What I actually needed was: Persistent Codex process │ ├── Turn │ │ │ ├── working │ ├── working │ ├── approval required │ │ ↓ │ │ WAIT │ │ ↓ │ │ approved │ │ │ ├── continue │ │ │ └── completed │ └── ready for next turn That led me to using Codex App Server . With an event-driven Codex integration, Express can receive lifecycle messages while the task is still running. An approval request is no longer equivalent to task completion. Instead: approval request ≠ task completed Codex can effectively tell the runner: "I'm still working on this turn, but I need a human decision before I can continue." The runner stores that pending approval and triggers a dedicated n8n webhook. This is probably my favorite part of the system. Suppose Codex needs permission. The request travels: Codex ↓ Express Runner ↓ n8n ↓ Cloudflare Tunnel ↓ Telegram On my phone I receive something like: ⚠️ Codex Approval Required Project: resume-web Action: