{
  "name": "AI Terraform PR Review & Security Agent",
  "nodes": [
    {
      "id": "0ada9836-12e6-41ff-bb4e-fb8914aaebe8",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -8016,
        5808
      ],
      "parameters": {
        "width": 1136,
        "height": 1008,
        "content": "## AI Terraform PR Review & Security Agent\n\n### How it works\n1. A GitHub pull request touching Terraform files triggers the workflow.\n2. An AI agent decides which checks to run — a plan diff, a security scan, a cost estimate, and a search of internal IaC standards.\n3. It writes a structured review with a pass/fail verdict per category and an overall risk rating.\n4. The review is posted back to the PR as a comment and logged to Postgres for audit history.\n5. Clean reviews auto-approve the PR; anything flagged high-risk routes to a human via Slack before it can merge.\n\n### Setup\n- [ ] Add GitHub credentials (repo scope, PR write access) on the trigger and all GitHub nodes\n- [ ] Add an OpenAI credential and a Vector Store credential for the RAG tool\n- [ ] Wire Tool - Run Terraform Plan and Tool - Run Security Scan to your CI runner — never let the agent apply, only plan/scan\n- [ ] Wire Tool - Estimate Cost Impact to Infracost's API or CLI output\n- [ ] Add a Postgres credential and create the `terraform_pr_reviews` table\n- [ ] Add Slack credentials on the security-review notification node\n- [ ] Point Wait - For Security Sign-Off's resume webhook at your reviewer UI's approve/request-changes action\n\n### Customization\nOpen a test PR touching a .tf file to run end-to-end, then push again to the same PR to confirm the sessionId keeps review context across revisions."
      },
      "typeVersion": 1
    },
    {
      "id": "04603ed9-0366-48c3-8892-cf97d4283d9d",
      "name": "Sticky Note - Trigger & Review Agent",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -6512,
        5712
      ],
      "parameters": {
        "color": 4,
        "width": 1552,
        "height": 1152,
        "content": "## Trigger and review\n\nFetches the PR's Terraform diff, then has an AI agent call whichever tools it needs — plan, security scan, cost estimate, IaC standards search — before writing a structured review."
      },
      "typeVersion": 1
    },
    {
      "id": "cb13888c-ba29-4c81-a733-05fb77de1806",
      "name": "Sticky Note - Logging & Auto-Gate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4880,
        5696
      ],
      "parameters": {
        "color": 4,
        "width": 928,
        "height": 944,
        "content": "## Log and post the review\n\nSaves the structured review for audit history, posts it as a PR comment, then checks whether it's clean enough to auto-approve."
      },
      "typeVersion": 1
    },
    {
      "id": "bb6a41b1-9126-426b-bbc2-cfcb392c7c55",
      "name": "Sticky Note - Human Security Gate & Merge",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3920,
        5696
      ],
      "parameters": {
        "color": 4,
        "width": 1264,
        "height": 944,
        "content": "## Approve or escalate to security\n\nAuto-approves clean PRs; flagged ones wait on a human security sign-off in Slack before merging or requesting changes."
      },
      "typeVersion": 1
    },
    {
      "id": "fb66a21b-a64f-4d88-9480-c8d97a1a08f7",
      "name": "GitHub Trigger - PR Opened/Updated",
      "type": "n8n-nodes-base.githubTrigger",
      "position": [
        -6464,
        6272
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "events": [
          "pull_request"
        ],
        "options": {},
        "repository": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2b07cc0a-4872-4afe-ac28-c583f27cd68e",
      "name": "Set - Config",
      "type": "n8n-nodes-base.set",
      "position": [
        -6240,
        6272
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "prNumber",
              "type": "string",
              "value": "={{ $json.pull_request.number }}"
            },
            {
              "name": "repoOwner",
              "type": "string",
              "value": "={{ $json.repository.owner.login }}"
            },
            {
              "name": "repoName",
              "type": "string",
              "value": "={{ $json.repository.name }}"
            },
            {
              "name": "branch",
              "type": "string",
              "value": "={{ $json.pull_request.head.ref }}"
            },
            {
              "name": "prAuthor",
              "type": "string",
              "value": "={{ $json.pull_request.user.login }}"
            },
            {
              "name": "prTitle",
              "type": "string",
              "value": "={{ $json.pull_request.title }}"
            },
            {
              "name": "sessionId",
              "type": "string",
              "value": "={{ $json.repository.full_name + '#' + $json.pull_request.number }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "96fa3c27-0577-48eb-8233-eb98961c8c11",
      "name": "Get - PR Diff",
      "type": "n8n-nodes-base.github",
      "position": [
        -6016,
        6272
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoOwner }}"
        },
        "resource": "pullRequest",
        "operation": "get",
        "repository": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoName }}"
        },
        "pullRequestNumber": "={{ $json.prNumber }}"
      },
      "typeVersion": 1
    },
    {
      "id": "5d79a36e-3ee3-41a6-a5f9-1942a4208b4e",
      "name": "Tool - Run Terraform Plan",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "position": [
        -5840,
        6704
      ],
      "parameters": {
        "url": "=https://ci.internal.example.com/api/terraform/plan",
        "method": "POST",
        "jsonBody": "={{ JSON.stringify({ repo: $fromAI('repo', 'owner/name of the repository'), branch: $fromAI('branch', 'the PR head branch to plan'), workingDir: $fromAI('working_dir', 'Terraform root module path within the repo, e.g. environments/prod') }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "toolDescription": "Trigger a read-only `terraform plan` on the PR branch (never apply) and return the resource-level diff: resources added/changed/destroyed, and any changed IAM/security-group/network rules. Use this to see the actual infrastructure impact rather than guessing from the raw HCL diff."
      },
      "typeVersion": 1.1
    },
    {
      "id": "975946ee-4d28-4776-8c87-06487b8adecf",
      "name": "Tool - Run Security Scan",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "position": [
        -5584,
        6704
      ],
      "parameters": {
        "url": "=https://ci.internal.example.com/api/terraform/security-scan",
        "method": "POST",
        "jsonBody": "={{ JSON.stringify({ repo: $fromAI('repo', 'owner/name of the repository'), branch: $fromAI('branch', 'the PR head branch to scan') }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "toolDescription": "Run a static security scan (tfsec/Checkov-style) against the PR branch's Terraform code and return findings: open security groups, unencrypted storage, public-facing resources, overly broad IAM policies, missing logging, hardcoded secrets, etc., each with a severity."
      },
      "typeVersion": 1.1
    },
    {
      "id": "7c5b05fe-205d-421b-b7f0-66b9ba2cfe3a",
      "name": "Tool - Estimate Cost Impact",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "position": [
        -5360,
        6704
      ],
      "parameters": {
        "url": "=https://ci.internal.example.com/api/terraform/cost-estimate",
        "method": "POST",
        "jsonBody": "={{ JSON.stringify({ repo: $fromAI('repo', 'owner/name of the repository'), branch: $fromAI('branch', 'the PR head branch to estimate cost for') }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "toolDescription": "Estimate the monthly cost delta this Terraform change would introduce (e.g. via Infracost). Use this to flag PRs with a significant unexpected cost increase."
      },
      "typeVersion": 1.1
    },
    {
      "id": "d901be7a-4a2d-43a9-a14d-8fc9ea658260",
      "name": "Tool - RAG Search IaC Standards",
      "type": "@n8n/n8n-nodes-langchain.toolVectorStore",
      "position": [
        -5232,
        6496
      ],
      "parameters": {
        "name": "search_iac_standards",
        "description": "Semantic search over the org's Terraform style guide, tagging/naming policy, module standards, and past PR review precedents. Use this to check the change against internal conventions, not just generic best practice."
      },
      "typeVersion": 1
    },
    {
      "id": "6a24cc26-fe2c-458e-8b3d-37f8f10740e3",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -5824,
        6496
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1"
        },
        "options": {},
        "builtInTools": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "f951da3d-5e85-4c5f-b4dc-f1dcaf272bf1",
      "name": "Window Buffer Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        -5680,
        6480
      ],
      "parameters": {
        "sessionKey": "={{ $json.sessionId }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 20
      },
      "typeVersion": 1.3
    },
    {
      "id": "de9bc6f0-1c13-467d-9a46-d2c7ca3f5217",
      "name": "AI Agent - Terraform Reviewer",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -5536,
        6272
      ],
      "parameters": {
        "text": "=You are a senior Platform/Security engineer reviewing a Terraform pull request before it can merge.\n\nRepo: {{ $json.repoOwner }}/{{ $json.repoName }} | Branch: {{ $json.branch }}\nPR #{{ $json.prNumber }} by {{ $json.prAuthor }}: \"{{ $json.prTitle }}\"\n\nYou have four tools: run_terraform_plan (resource-level diff), run_security_scan (static security findings), estimate_cost_impact (monthly cost delta), search_iac_standards (org conventions/precedent). Use whichever combination is relevant — call multiple tools, and call them again with different parameters if needed — before writing your review. Do not guess plan output, scan findings, or cost numbers; only state what the tools returned.\n\nIf earlier turns exist in this conversation (same PR), treat this as a re-review after a new push and note what changed since the prior review.\n\nCategorize findings under: security, cost, correctness (will the plan do what the PR claims), and standards (naming/tagging/module conventions). Assign each finding a severity ('low'|'medium'|'high'|'critical'). Set overallRisk to the highest severity present. List any 'critical' or 'high' security finding, or any plan output showing a destroy of a stateful resource (database, volume) as a blockingIssue requiring human sign-off.\n\nReturn ONLY a JSON object (no markdown fences, no commentary) with keys: summary (2-4 sentences), findings (array of {category, description, severity}), overallRisk ('low'|'medium'|'high'|'critical'), verdict ('pass'|'flag_for_review'), blockingIssues (array of strings, empty if none), costImpactSummary (string).",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "ada12ec7-e540-4298-ba8e-417b42fa7302",
      "name": "Set - Parse Agent Output",
      "type": "n8n-nodes-base.set",
      "position": [
        -4784,
        6272
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "parsedOutput",
              "type": "object",
              "value": "={{ JSON.parse(String($json.output || $json.text || $json.response || '{}').replace(/```json|```/g, '').trim()) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "88c2ed35-933c-4837-b257-111b07a5b770",
      "name": "Postgres - Log PR Review",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -4560,
        6272
      ],
      "parameters": {
        "table": "terraform_pr_reviews",
        "schema": "public",
        "columns": "pr_number, repo, author, findings, overall_risk, verdict, blocking_issues, cost_impact_summary, created_at",
        "options": {}
      },
      "typeVersion": 2.5
    },
    {
      "id": "93b008be-3faf-497e-9922-d58f9f2f64b0",
      "name": "GitHub - Post Review Comment",
      "type": "n8n-nodes-base.github",
      "position": [
        -4336,
        6272
      ],
      "parameters": {
        "body": "=### 🤖 Terraform Review — Verdict: {{ $json.parsedOutput.verdict.toUpperCase() }} (risk: {{ $json.parsedOutput.overallRisk }})\n\n{{ $json.parsedOutput.summary }}\n\n**Findings**\n{{ $json.parsedOutput.findings.map(f => '- [' + f.severity + '] (' + f.category + ') ' + f.description).join('\\n') }}\n\n**Cost impact:** {{ $json.parsedOutput.costImpactSummary }}\n\n{{ $json.parsedOutput.blockingIssues.length ? '**Blocking issues requiring human sign-off:**\\n' + $json.parsedOutput.blockingIssues.join('\\n') : 'No blocking issues.' }}",
        "owner": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoOwner }}"
        },
        "operation": "createComment",
        "repository": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoName }}"
        },
        "issueNumber": "={{ $json.prNumber }}"
      },
      "typeVersion": 1
    },
    {
      "id": "19ded8b5-b00b-4ad1-ae4f-17cee01a54ef",
      "name": "IF - Review Passed",
      "type": "n8n-nodes-base.if",
      "position": [
        -4112,
        6272
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.parsedOutput.verdict }}",
              "rightValue": "pass"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d230987c-b58c-4d93-bb1c-9ddfd4ea4068",
      "name": "GitHub - Approve PR (auto)",
      "type": "n8n-nodes-base.github",
      "position": [
        -3888,
        6176
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoOwner }}"
        },
        "resource": "review",
        "repository": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoName }}"
        },
        "additionalFields": {},
        "pullRequestNumber": "={{ $json.prNumber }}"
      },
      "typeVersion": 1
    },
    {
      "id": "0fc7dd74-95f2-4ad8-84e2-29dfc1f99c04",
      "name": "Slack - Request Security Review",
      "type": "n8n-nodes-base.slack",
      "position": [
        -3888,
        6368
      ],
      "parameters": {
        "text": "=🔒 *Terraform PR flagged for security review*\n\n*PR:* {{ $json.repoOwner }}/{{ $json.repoName }}#{{ $json.prNumber }} — \"{{ $json.prTitle }}\" by {{ $json.prAuthor }}\n*Overall risk:* {{ $json.parsedOutput.overallRisk }}\n\n*Blocking issues:*\n{{ $json.parsedOutput.blockingIssues.join('\\n') }}\n\n*All findings:*\n{{ $json.parsedOutput.findings.map(f => '- [' + f.severity + '] (' + f.category + ') ' + f.description).join('\\n') }}\n\nReply or use the Approve/Request-Changes action to make the merge decision.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#terraform-security-review"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "431c5f95-a364-413b-8f8a-44d64c00a4bb",
      "name": "Wait - For Security Sign-Off",
      "type": "n8n-nodes-base.wait",
      "position": [
        -3664,
        6368
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "f1d97882-6a9c-48aa-b460-9f9384308446",
      "name": "IF - Security Approved",
      "type": "n8n-nodes-base.if",
      "position": [
        -3440,
        6368
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.securityApproved === true || $json.securityApproved === 'true' }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e29a21c7-b5bd-46ba-9503-f3158195c31c",
      "name": "GitHub - Approve PR (delayed)",
      "type": "n8n-nodes-base.github",
      "position": [
        -3216,
        6272
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoOwner }}"
        },
        "resource": "review",
        "repository": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoName }}"
        },
        "additionalFields": {},
        "pullRequestNumber": "={{ $json.prNumber }}"
      },
      "typeVersion": 1
    },
    {
      "id": "92a3879d-7860-486e-ba5f-2ca9b32b4f4a",
      "name": "GitHub - Request Changes",
      "type": "n8n-nodes-base.github",
      "position": [
        -3216,
        6464
      ],
      "parameters": {
        "body": "=Changes requested after security review. Blocking issues:\n{{ $json.parsedOutput.blockingIssues.join('\\n') }}",
        "event": "requestChanges",
        "owner": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoOwner }}"
        },
        "resource": "review",
        "repository": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.repoName }}"
        },
        "additionalFields": {},
        "pullRequestNumber": "={{ $json.prNumber }}"
      },
      "typeVersion": 1,
      "continueOnFail": true
    }
  ],
  "connections": {
    "Set - Config": {
      "main": [
        [
          {
            "node": "Get - PR Diff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get - PR Diff": {
      "main": [
        [
          {
            "node": "AI Agent - Terraform Reviewer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent - Terraform Reviewer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "IF - Review Passed": {
      "main": [
        [
          {
            "node": "GitHub - Approve PR (auto)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack - Request Security Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Window Buffer Memory": {
      "ai_memory": [
        [
          {
            "node": "AI Agent - Terraform Reviewer",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "IF - Security Approved": {
      "main": [
        [
          {
            "node": "GitHub - Approve PR (delayed)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "GitHub - Request Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres - Log PR Review": {
      "main": [
        [
          {
            "node": "GitHub - Post Review Comment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set - Parse Agent Output": {
      "main": [
        [
          {
            "node": "Postgres - Log PR Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool - Run Security Scan": {
      "ai_tool": [
        [
          {
            "node": "AI Agent - Terraform Reviewer",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Tool - Run Terraform Plan": {
      "ai_tool": [
        [
          {
            "node": "AI Agent - Terraform Reviewer",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Tool - Estimate Cost Impact": {
      "ai_tool": [
        [
          {
            "node": "AI Agent - Terraform Reviewer",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "GitHub - Post Review Comment": {
      "main": [
        [
          {
            "node": "IF - Review Passed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - For Security Sign-Off": {
      "main": [
        [
          {
            "node": "IF - Security Approved",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent - Terraform Reviewer": {
      "main": [
        [
          {
            "node": "Set - Parse Agent Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack - Request Security Review": {
      "main": [
        [
          {
            "node": "Wait - For Security Sign-Off",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool - RAG Search IaC Standards": {
      "ai_tool": [
        [
          {
            "node": "AI Agent - Terraform Reviewer",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "GitHub Trigger - PR Opened/Updated": {
      "main": [
        [
          {
            "node": "Set - Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}
