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 →
{
"name": "My workflow",
"nodes": [
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "bf4b0740-581a-4fe9-880a-f622a07bef1c",
"leftValue": "={{$json.body.action}}",
"rightValue": "opened",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
},
{
"id": "87f6c536-1262-49bb-922d-9e2cfb41832e",
"leftValue": "={{$json.body.action}}",
"rightValue": "synchronize",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "or"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
16,
-32
],
"id": "10e0f8d7-2f43-46e8-9658-862b2ba7fcae",
"name": "If"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
208,
128
],
"id": "40a8619f-3f4b-4594-b8c6-46956ed75a34",
"name": "No Operation, do nothing"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"leftValue": "={{ $json.verdict }}",
"rightValue": "approve",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "0a81acba-ba03-40cc-a562-ac1cfeced79f"
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "b55bfd5d-df41-4910-a91b-9fe0ffa13db2",
"leftValue": "={{ $json.verdict }}",
"rightValue": "comment",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "91b2cf24-3596-43dd-b863-c55859c032ff",
"leftValue": "={{ $json.verdict }}",
"rightValue": "request_changes",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
1072,
-128
],
"id": "1e7cb475-81db-4623-9899-c245c25545b8",
"name": "Switch"
},
{
"parameters": {
"httpMethod": "POST",
"path": "pr-review",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-192,
-32
],
"id": "c31fed3e-a066-43e1-b596-3b6a4b11a967",
"name": "Raw Data From Github PR"
},
{
"parameters": {
"url": "={{ $json.body.action === 'synchronize' \n ? `https://api.github.com/repos/${$json.body.repository.full_name}/compare/${$json.body.before}...${$json.body.after}` \n : `https://api.github.com/repos/${$json.body.repository.full_name}/pulls/${$json.body.pull_request.number}/files?per_page=100` \n}}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_GITHUB_TOKEN_HERE"
},
{
"name": "Accept",
"value": "application/vnd.github+json"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
240,
-128
],
"id": "eab23790-1331-4eb1-b03a-dcf40d1c810d",
"name": "Fetch File Changes"
},
{
"parameters": {
"jsCode": "const rawItems = $input.all().map(item => item.json);\n\nlet files;\nif (rawItems.length === 1 && rawItems[0].files) {\n // Came from the compare endpoint - files are nested inside one object\n files = rawItems[0].files;\n} else {\n // Came from the pulls/files endpoint - already a flat array of file items\n files = rawItems;\n}\n\nconst skip = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];\n\nlet diffText = '';\nfor (const file of files) {\n if (skip.includes(file.filename)) continue;\n if (!file.patch) continue;\n diffText += `\\n\\n--- FILE: ${file.filename} ---\\n${file.patch}`;\n}\n\nconst webhookData = $('Raw Data From Github PR').first().json.body;\n\nreturn [{\n json: {\n diffText: diffText || 'No reviewable changes found.',\n prNumber: webhookData.pull_request.number,\n repoFullName: webhookData.repository.full_name\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
448,
-128
],
"id": "4f5bb74e-d93e-4f4e-8112-cf499652df27",
"name": "Parsing Raw Data"
},
{
"parameters": {
"method": "POST",
"url": "https://openrouter.ai/api/v1/chat/completions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_OPENROUTER_API_KEY_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({\n model: \"openai/gpt-oss-20b:free\",\n messages: [\n {\n role: \"system\",\n content: \"You are a senior, production-minded code reviewer. Review the given diff the way a staff engineer would before approving a merge: think about what could break in production, not just style.\\n\\nSpecifically look for:\\n- Logic bugs and incorrect behavior\\n- Security/authorization issues (e.g. data leaking across users, missing auth checks, trusting client-controlled input)\\n- Broken functionality introduced by the change\\n- Missed edge cases or error handling gaps\\n- Clear, material performance or architectural optimization opportunities (skip micro-optimizations)\\n\\nClassify every issue found into exactly one severity:\\n- \\\"blocker\\\": will break functionality, security, or correctness in production. Must be fixed before merge.\\n- \\\"suggestion\\\": a real improvement (clarity, missing tests, optimization) but not merge-blocking.\\n- \\\"nit\\\": minor/cosmetic.\\n\\nFor each issue provide:\\n- file: filename it relates to\\n- issue: a clear, specific, detailed description of what is wrong\\n- risk: BRIEF (1-2 sentences) - what could actually break because of it\\n- fix: BRIEF (1-2 sentences) - the direction of the fix, not full code\\n\\nRespond ONLY with valid JSON, no markdown fences, no text outside the JSON:\\n{\\n \\\"verdict\\\": \\\"approve|comment|request_changes\\\",\\n \\\"summary\\\": \\\"2-3 sentence overview of the PR's overall state and the main risk if any\\\",\\n \\\"blockers\\\": [{\\\"file\\\":\\\"...\\\", \\\"issue\\\":\\\"...\\\", \\\"risk\\\":\\\"...\\\", \\\"fix\\\":\\\"...\\\"}],\\n \\\"suggestions\\\": [{\\\"file\\\":\\\"...\\\", \\\"issue\\\":\\\"...\\\", \\\"risk\\\":\\\"...\\\", \\\"fix\\\":\\\"...\\\"}],\\n \\\"nits\\\": [{\\\"file\\\":\\\"...\\\", \\\"issue\\\":\\\"...\\\", \\\"risk\\\":\\\"...\\\", \\\"fix\\\":\\\"...\\\"}]\\n}\\n\\nRules:\\n- verdict = \\\"request_changes\\\" if blockers is non-empty.\\n- verdict = \\\"comment\\\" if blockers is empty but suggestions or nits exist.\\n- verdict = \\\"approve\\\" only if blockers, suggestions, and nits are ALL empty.\\n- Only comment on what the diff itself shows. Do not invent issues that aren't present. Do not fabricate test results, build status, or verification steps you have not actually run - you only have the diff, not execution access.\"\n },\n {\n role: \"user\",\n content: $json.diffText\n }\n ]\n}) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
656,
-128
],
"id": "7fe32d92-142b-4281-9e49-9ab2dba47f54",
"name": "Analyzing PR"
},
{
"parameters": {
"jsCode": "const response = $input.first().json;\nconst rawContent = response.choices[0].message.content;\n\nlet parsed;\ntry {\n parsed = JSON.parse(rawContent);\n} catch (e) {\n parsed = {\n verdict: 'comment',\n summary: 'Could not parse model response.',\n blockers: [], suggestions: [],\n nits: [{ file: 'n/a', issue: 'Parse failure', risk: 'n/a', fix: 'Raw output: ' + rawContent.slice(0, 200) }]\n };\n}\n\nfunction formatSection(title, items) {\n if (!items || items.length === 0) return '';\n let text = `\\n\\n**${title}**\\n`;\n for (const item of items) {\n text += `\\n- **${item.file}** \u2014 ${item.issue}\\n - Risk: ${item.risk}\\n - Fix: ${item.fix}\\n`;\n }\n return text;\n}\n\nlet body = `**Summary**: ${parsed.summary}`;\nbody += formatSection('\ud83d\udd34 Blockers', parsed.blockers);\nbody += formatSection('\ud83d\udfe1 Suggestions', parsed.suggestions);\nbody += formatSection('\ud83d\udcad Nits', parsed.nits);\n\nreturn [{\n json: {\n verdict: parsed.verdict,\n body: body,\n prNumber: $('Parsing Raw Data').first().json.prNumber,\n repoFullName: $('Parsing Raw Data').first().json.repoFullName\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
864,
-128
],
"id": "1d8bb6dd-7c33-48ae-b9b8-718633dc9fd3",
"name": "Parse LLM Response"
},
{
"parameters": {
"method": "POST",
"url": "=https://api.github.com/repos/{{ $json.repoFullName }}/pulls/{{ $json.prNumber }}/reviews",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_GITHUB_TOKEN_HERE"
},
{
"name": "Accept",
"value": "application/vnd.github+json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ body: $json.body, event: \"APPROVE\" }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1280,
-240
],
"id": "1684416b-d54f-4d04-99d8-4e58bc3ce63b",
"name": "Accept Push"
},
{
"parameters": {
"method": "POST",
"url": "=https://api.github.com/repos/{{ $json.repoFullName }}/pulls/{{ $json.prNumber }}/reviews",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_GITHUB_TOKEN_HERE"
},
{
"name": "Accept",
"value": "application/vnd.github+json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ body: $json.body, event: \"COMMENT\" }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1280,
-80
],
"id": "eda3c632-6158-4235-9ca6-07fcde469092",
"name": "Comment Push"
},
{
"parameters": {
"method": "POST",
"url": "=https://api.github.com/repos/{{ $json.repoFullName }}/pulls/{{ $json.prNumber }}/reviews",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_GITHUB_TOKEN_HERE"
},
{
"name": "Accept",
"value": "application/vnd.github+json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ body: $json.body, event: \"REQUEST_CHANGES\" }) }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1280,
80
],
"id": "38eb6138-42a9-455b-860c-e6c4f9dac54c",
"name": "Request Changes Push"
}
],
"connections": {
"If": {
"main": [
[
{
"node": "Fetch File Changes",
"type": "main",
"index": 0
}
],
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
},
"No Operation, do nothing": {
"main": [
[]
]
},
"Switch": {
"main": [
[
{
"node": "Accept Push",
"type": "main",
"index": 0
}
],
[
{
"node": "Comment Push",
"type": "main",
"index": 0
}
],
[
{
"node": "Request Changes Push",
"type": "main",
"index": 0
}
]
]
},
"Raw Data From Github PR": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Fetch File Changes": {
"main": [
[
{
"node": "Parsing Raw Data",
"type": "main",
"index": 0
}
]
]
},
"Parsing Raw Data": {
"main": [
[
{
"node": "Analyzing PR",
"type": "main",
"index": 0
}
]
]
},
"Analyzing PR": {
"main": [
[
{
"node": "Parse LLM Response",
"type": "main",
"index": 0
}
]
]
},
"Parse LLM Response": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"versionId": "8696faa7-32a8-4ba8-8cfd-033dff67aca1",
"nodeGroups": [],
"id": "FQ73vAAcoZAB18xn",
"tags": []
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Pr-Reviewer. Uses httpRequest. Webhook trigger; 11 nodes.
Source: https://github.com/Tanmay-Awal/ai-pr-reviewer-agent/blob/0dc971d5c94d4c0ef2a026687910072b7a115f2d/workflows/pr-reviewer.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Jigsaw API key for image processing, I use this as a gatekeeper/second pair of eyes. LINK to their website https://jigsawstack.com/ SECOND A postgress DATABASE (I use Supabase) LlamaCloud for the pars
Onsite Photos to Jobs (SMS Agent). Uses dataTable, twilio, httpRequest, airtable. Webhook trigger; 62 nodes.
W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 50 nodes.
W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 48 nodes.