Parallel LangGraph Agents That Review Pull Requests via FastAPI Webhooks
How a webhook, Redis queue and four LangGraph agents turn a pull request into a scored, structured AI review in about 30 seconds, with ngrok for local testing.
This article is published in English.
Sources: Building a Multi-Agent PR Code Review System With LangGraph, FastAPI, Webhooks, and Docker
Anyone who has opened a pull request knows the wait. A feature is done, the code is pushed, and then comes two hours, or a full day, or longer. When a teammate finally reviews it, there are a dozen comments and half of them are things software could have caught: a hardcoded API key, a function called x, a SQL query wide open to injection. The author fixes everything, pushes again and waits again.
Code review itself is essential, so that isn't the complaint. The frustration is that so much first-pass feedback is mechanical, the same categories of issue on every PR. The idea behind this system is to let an AI catch all the obvious material immediately so human reviewers can spend their time on architecture and logic, where human judgment is actually needed. It works better than might be expected.
Why several agents beat one
The naive design sends the PR diff to a model with an instruction to review it. That is fine for small changes. As PRs grow, one model asked to think about security, performance and code quality all at once loses focus and the findings degrade.
Specialization fixes this. Give each agent a single job: one that thinks only about security, one only about performance, one only about code quality, and a fourth that takes the three reports and writes a clean, professional review. This multi-agent pattern gives markedly better results than one generalist agent.
The overall design, with each component owning one responsibility:
You open a Pull Request on GitHub
│
▼
GitHub fires a webhook
│
▼
FastAPI receives it instantly
└── Verifies HMAC signature
└── Pushes job to Redis queue
└── Returns 202 to GitHub ✅
│
▼
ARQ Worker picks up the job
│
▼
LangGraph Pipeline starts
│
┌─────────┼─────────┐
▼ ▼ ▼
Agent 1 Agent 2 Agent 3
Quality Security Performance
│ │ │
└─────────┼─────────┘
▼
Agent 4
Final Reviewer
│
▼
Comment posted on your PR ✅
(in under 30 seconds)
Step 1: the GitHub webhook
Everything starts with a webhook. When a pull request is opened or updated, GitHub sends an HTTP POST to a URL you configure, and the request carries the PR number, title, author, repository and the action that triggered it. A FastAPI server listens at that URL.
The server's first act on any request is to verify the signature. GitHub signs each payload with an HMAC-SHA256 hash using a secret you define, and a mismatch means the request is rejected immediately, so nobody can send fake PR events to the endpoint.
The next decision is the critical one: the server doesn't begin the AI review right away. It pushes a job onto a Redis queue and answers GitHub with 202 Accepted in under a second. The reason is that GitHub expects a webhook response within 10 seconds, and a full review with three parallel LLM calls takes 25 to 30. Reviewing inline would make GitHub think the server failed and retry, so the PR would be reviewed several times with duplicate comments. A job queue solves that outright. GitHub gets its quick answer and the real work happens in the background.
Step 2: ngrok for local development
During local development the FastAPI server runs at localhost:8000, which GitHub can't reach from the internet. ngrok fills the gap by creating a secure tunnel between a public URL and the local machine. One command starts it:
ngrok http 8000
It immediately hands back a public https address such as:
https://abc123.ngrok-free.app
Paste that address into the GitHub webhook settings and GitHub can deliver events straight to the laptop. Each time a PR opens, the request travels from GitHub's servers through ngrok's servers to the local FastAPI app. In the terminal the whole exchange appears as a single line:
23:03:02 POST /api/webhook 202 Accepted ✅
One line, under a second, and GitHub is satisfied while the real work begins behind the scenes. One caution: the free tier of ngrok issues a new URL on every restart, so the GitHub webhook URL has to be updated each time.
Step 3: the LangGraph pipeline
This is the core of the system. LangGraph is a framework for stateful, multi-agent workflows. Agents become nodes in a graph, edges connect them, and shared state flows between them. Picture a flowchart where every box is an AI agent and the arrows say what data moves where and in what order.
Shared state
All agents read from and write to one shared state object holding the PR number and repository details, the fetched diff with the actual code changes, each agent's review output and the final synthesized comment. It moves through the graph like a baton in a relay, with each agent adding its piece before passing it on.
Node 0: fetch the PR
A dedicated node runs before any agent and queries the GitHub API for the PR's title, author and branch along with the complete diff of each changed file. It converts all of that into one tidy text string the LLMs can read, and the same formatted diff is passed to each of the reviewers.
The parallel fan-out: agents 1, 2 and 3
After the fetch, the graph fans out to three agents at once. They receive the same diff, run at the same moment and know nothing about each other.
The code quality agent lives entirely in its own world. Its system prompt covers readability, naming conventions, DRY violations, error handling and maintainability, and it returns structured JSON with a score out of 10 plus a list of specific issues, each with file name, line number and suggested fix.
The security agent looks only for vulnerabilities: SQL injection from unsanitized input, hardcoded keys, passwords or tokens, missing authentication checks, and sensitive data in logs. It knows nothing about performance or style.
The performance agent thinks only about efficiency: N+1 database query patterns, nested loops of O(n²) complexity or worse, blocking IO calls inside async functions, and missing indexes or caching opportunities.
Parallelism matters for time. Run sequentially, the three agents would need 60 to 90 seconds in total. Run together, they finish in roughly 7 to 12 seconds, which is the time of the slowest single agent and not the sum of all three.
The fan-in: agent 4
The final reviewer waits automatically until all three specialists are finished. LangGraph handles that synchronization, so no waiting or polling logic has to be written. When the three reports arrive as structured JSON, agent 4 does one thing and writes the final review comment in Markdown.
Agent 4 performs no analysis. It never revisits the code and only combines and formats what the specialists reported. That's deliberate: even if its wording wobbles a bit, the findings underneath remain exactly what the specialists produced. Its comment opens with an overall verdict and a score that is the mean of the three agents' scores, then summarizes what each agent found, and ends with a Must Fix Before Merge list restricted to critical and high-severity issues.
A gotcha in parallel state
A subtle technical detail is easy to overlook. Once the parallel agents complete and write back into shared state, each one must hand back only the key it owns. Should every agent return the entire state object, LangGraph gets three clashing copies of every shared field simultaneously and throws an InvalidUpdateError. The remedy is simple yet not obvious: attach an Annotated reducer to the fields that parallel agents write, and have each agent return only its own output. You learn this by running into the error, and most LangGraph tutorials never mention it.
Step 4: the output
The proof is in a test. A Python file containing four deliberate problems, a hardcoded secret, a SQL injection hole, an O(n²) nested loop and poor naming, was pushed to a branch and opened as a pull request. A review appeared on the PR 27 seconds later. This was the code that went in:
import sqlite3
# Hardcoded secret — security agent should catch this
API_KEY = "sk-prod-supersecretkey123"
def get_user(user_id):
conn = sqlite3.connect("users.db")
# SQL injection — security agent should catch this
query = f"SELECT * FROM users WHERE id = {user_id}"
return conn.execute(query)
def process(users):
# O(n²) nested loop — performance agent should catch this
for user in users:
for item in users:
print(user, item)
# Bad naming — quality agent should catch this
def x(a, b, c):
return a + b + c
And this is what the system posted on the PR:
## 🤖 AI Code Review
**Overall Verdict:** ❌ REJECTED
**Score:** 2.33/10
---
### 📋 Code Quality
Score: 2/10 | 8 Issues Found
Function naming is completely unclear — x, a, b, c give no
indication of purpose. No error handling on database operations.
No docstrings or type hints on any function.
### 🔒 Security
Score: 2/10 | 4 Critical Vulnerabilities
Hardcoded API key on line 3 exposes credentials in version
control — CRITICAL. SQL query on line 9 directly interpolates
user input, creating a SQL injection vulnerability — CRITICAL.
### ⚡ Performance
Score: 3/10 | 4 Issues Found
Nested loop in process() creates O(n²) complexity — will become
extremely slow as the users list grows. Database connection is
opened but never closed, creating a connection leak.
---
### 🚨 Must Fix Before Merge
- Remove API_KEY hardcoding — use environment variables instead
- Parameterize the SQL query to prevent injection attacks
- Refactor nested loop — use set operations or single pass
- Close database connections properly or use a context manager
---
Great effort on the overall structure! Addressing these critical
issues will significantly improve the security and reliability
of this code. Looking forward to the revised version. 🙌
The review caught four real categories of problem with specific line references and fix suggestions, 27 seconds after the pull request was created. No human was involved and no waiting happened, just structured, actionable feedback straight away.
Running it locally
Everything starts with one Docker command: environment variables get set, a GitHub token is created and the webhook configured, ngrok connects the local server to GitHub, and a test PR shows the review appearing live. Switching between Claude and OpenAI takes a single environment variable, and the setup steps are documented in the repository's README.
Lessons from the build
Narrow agents outperform generalists. One job per agent produces sharper, more targeted findings, because the security agent isn't pulled toward code style and the performance agent spends no tokens on variable names.
The job queue is part of the architecture and not an option. GitHub's 10-second timeout is strict, and without Redis plus a background worker, failed deliveries get retried and duplicate reviews pile up. Many tutorials skip this piece, yet it's what makes the system reliable.
Parallel execution changes the arithmetic. Three sequential calls of 10 seconds each need at least 30 seconds, while three parallel ones need 10. LangGraph makes the parallelism trivial to write, and the difference is dramatic.
Structured output is what makes agents production-ready. Every agent returns a strict JSON schema, and if parsing fails, the job fails cleanly and nothing gets posted to GitHub. The system either yields a good review or nothing, and never garbage.
Abstract the LLM from day one. A factory pattern that switches between Claude and OpenAI through one environment variable takes about ten minutes to build and saves hours of refactoring later, since models can be tried and their review quality compared freely.
Where it leaves code review
Code review isn't going away, and for architecture decisions, business logic and delicate design tradeoffs nothing replaces a human. The mechanical first pass is another matter: hardcoded secrets, SQL injections, quadratic loops and unclear names can all be automated, which frees human reviewers for work that needs human intelligence. This system catches the obvious problems in about 30 seconds so teammates can focus on the important ones.