{
  "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": []
}