{
  "id": "QAdTsYgeqsD381Ep",
  "name": "Analyze n8n workflow JSON for risks and best practices with Groq",
  "tags": [],
  "nodes": [
    {
      "id": "f57c8482-cd77-4dbb-8b96-70481109d474",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        -128
      ],
      "parameters": {
        "width": 480,
        "height": 608,
        "content": "## Analyze n8n workflow JSON for risks and best practices with Groq\n\n### How it works\n\nThis workflow exposes a webhook that accepts an n8n workflow JSON payload, validates that the submitted data is usable, and branches based on the validation result. Valid workflows are sent to an AI reviewer powered by Groq and a structured output parser to identify risks and best-practice issues. Invalid submissions are converted into a formatted error response, and both success and error paths return through the same webhook response node.\n\n### Setup steps\n\n- Configure the webhook trigger path, HTTP method, and authentication settings appropriate for how callers will submit workflow JSON.\n- Review the validation code so it matches the expected request body shape and required workflow fields.\n- Add and test Groq credentials for the Groq Chat Model, and select the desired model in the AI agent configuration.\n- Confirm the Structured Output Parser schema matches the response format expected by API consumers.\n- Test both valid and invalid workflow submissions to verify the final webhook response format.\n\n### Customization\n\nYou can customize the AI reviewer prompt, risk categories, severity scale, and structured output schema to match your internal n8n review standards."
      },
      "typeVersion": 1
    },
    {
      "id": "870fd60f-69b0-4786-b66a-a76ef613f7b0",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        448,
        192
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 320,
        "content": "## Receive and validate input\n\nAccepts the incoming workflow JSON through the webhook, runs custom validation logic, and decides whether the payload can proceed to AI review or should follow the error branch."
      },
      "typeVersion": 1
    },
    {
      "id": "7225646f-f334-45d6-898d-356f85465294",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1248,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 352,
        "height": 576,
        "content": "## Review workflow with AI\n\nUses the AI agent, Groq chat model, and structured output parser to analyze valid workflow JSON for risks, quality issues, and best-practice recommendations."
      },
      "typeVersion": 1
    },
    {
      "id": "2dc67a47-bedb-4fe0-ae67-51f89101fd5c",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1312,
        512
      ],
      "parameters": {
        "color": 7,
        "height": 384,
        "content": "## Format validation errors\n\nHandles the lower error branch by turning failed validation results into a consistent response payload."
      },
      "typeVersion": 1
    },
    {
      "id": "04fffe2a-ff44-4226-8517-819167f196c6",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1744,
        64
      ],
      "parameters": {
        "color": 7,
        "height": 336,
        "content": "## Return webhook response\n\nSends the final analysis or formatted validation error back to the original webhook caller."
      },
      "typeVersion": 1
    },
    {
      "id": "4e1ee93e-35e9-4fc5-bb04-6a69b409e9f1",
      "name": "When Workflow Received",
      "type": "n8n-nodes-base.webhook",
      "position": [
        496,
        352
      ],
      "parameters": {
        "path": "workflow",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "9351405c-413e-4c43-aef1-ca756e21f4c5",
      "name": "Send Analysis Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1792,
        240
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{$json}}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "e85953a1-2164-448e-87fb-583571c56fbb",
      "name": "Validate Workflow Data",
      "type": "n8n-nodes-base.code",
      "position": [
        720,
        352
      ],
      "parameters": {
        "jsCode": "const raw = $json.body ?? $json;\n\ntry {\n  if (!raw.workflow) throw new Error(\"Missing 'workflow' field in request body\");\n\n  const wf = raw.workflow;\n  const nodes = wf.nodes;\n  const connections = wf.connections;\n\n  if (!Array.isArray(nodes) || nodes.length === 0)\n    throw new Error(\"No nodes found in workflow\");\n\n  if (!connections || typeof connections !== \"object\")\n    throw new Error(\"No connections object found in workflow\");\n\n  // ---- Build helper lookups ----\n  const nodeNames = nodes.map(n => n.name);\n  const sourceNames = Object.keys(connections);\n\n  // Nodes that appear as a target anywhere in connections (across ALL connection\n  // types: main, ai_languageModel, ai_outputParser, ai_tool, ai_memory, etc.)\n  const targetNames = new Set();\n  for (const src of sourceNames) {\n    const connectionTypes = connections[src] ?? {};\n    for (const outputGroups of Object.values(connectionTypes)) {\n      for (const group of (outputGroups ?? [])) {\n        for (const conn of (group ?? [])) {\n          if (conn?.node) targetNames.add(conn.node);\n        }\n      }\n    }\n  }\n\n  const triggerTypes = [\n    \"n8n-nodes-base.webhook\",\n    \"n8n-nodes-base.scheduleTrigger\",\n    \"n8n-nodes-base.manualTrigger\",\n    \"n8n-nodes-base.cronTrigger\",\n    \"n8n-nodes-base.errorTrigger\"\n  ];\n\n  const deprecatedTypes = [\n    \"n8n-nodes-base.function\",\n    \"n8n-nodes-base.functionItem\"\n  ];\n\n  const credentialBearingTypes = new Set(\n    nodes.filter(n => n.credentials).map(n => n.type)\n  );\n\n  // ---- Findings ----\n  const findings = [];\n\n  // 1. Orphan nodes: not a trigger, not a LangChain sub-node provider (model/\n  // parser/tool/memory \u2014 these have no incoming connection by design), and has\n  // no incoming connection from any other node.\n  const isLangchainSubNode = (type) =>\n    type.startsWith(\"@n8n/n8n-nodes-langchain.lm\") ||\n    type.startsWith(\"@n8n/n8n-nodes-langchain.outputParser\") ||\n    type.startsWith(\"@n8n/n8n-nodes-langchain.memory\") ||\n    type.startsWith(\"@n8n/n8n-nodes-langchain.tool\") ||\n    type.startsWith(\"@n8n/n8n-nodes-langchain.embeddings\") ||\n    type.startsWith(\"@n8n/n8n-nodes-langchain.vectorStore\") ||\n    type.startsWith(\"@n8n/n8n-nodes-langchain.document\");\n\n  const orphanNodes = nodes.filter(n => {\n    const isTrigger = triggerTypes.includes(n.type) || /trigger/i.test(n.type);\n    if (isTrigger || isLangchainSubNode(n.type)) return false;\n    return !targetNames.has(n.name);\n  });\n  if (orphanNodes.length > 0) {\n    findings.push({\n      type: \"orphan_node\",\n      severity: \"medium\",\n      nodes: orphanNodes.map(n => n.name),\n      detail: \"Node(s) are not triggers but have no incoming connection from any other node.\"\n    });\n  }\n\n  // 2. Nodes with no outgoing connection (dead ends), excluding known terminal node types\n  const terminalTypes = [\n    \"n8n-nodes-base.respondToWebhook\",\n    \"n8n-nodes-base.noOp\"\n  ];\n  const deadEndNodes = nodes.filter(n => {\n    if (terminalTypes.includes(n.type)) return false;\n    // LangChain sub-nodes (chat models, output parsers, tools, memory) are\n    // legitimately terminal \u2014 they feed an agent via a side-channel and have\n    // no further \"main\" output by design.\n    if (isLangchainSubNode(n.type)) return false;\n\n    const connectionTypes = connections[n.name] ?? {};\n    const hasOutgoing = Object.values(connectionTypes).some(\n      outputGroups => (outputGroups ?? []).some(group => (group ?? []).length > 0)\n    );\n    return !hasOutgoing;\n  });\n  if (deadEndNodes.length > 0) {\n    findings.push({\n      type: \"dead_end_node\",\n      severity: \"low\",\n      nodes: deadEndNodes.map(n => n.name),\n      detail: \"Node(s) have no outgoing connection and are not a recognized terminal node type.\"\n    });\n  }\n\n  // 3. No error-handling path anywhere in the workflow\n  const hasErrorTrigger = nodes.some(n => n.type === \"n8n-nodes-base.errorTrigger\");\n  const hasOnErrorContinue = nodes.some(n => n.onError === \"continueRegularOutput\" || n.onError === \"continueErrorOutput\");\n  if (!hasErrorTrigger && !hasOnErrorContinue) {\n    findings.push({\n      type: \"no_error_handling\",\n      severity: \"high\",\n      nodes: [],\n      detail: \"Workflow has no Error Trigger node and no node configured with onError continue behavior.\"\n    });\n  }\n\n  // 4. Hardcoded / placeholder credential IDs (common copy-paste leftovers)\n  const placeholderCredPatterns = /^(REPLACE_ME|TODO|xxxx|placeholder)/i;\n  nodes.forEach(n => {\n    if (n.credentials) {\n      Object.entries(n.credentials).forEach(([credType, cred]) => {\n        if (cred?.id && placeholderCredPatterns.test(cred.id)) {\n          findings.push({\n            type: \"placeholder_credential\",\n            severity: \"medium\",\n            nodes: [n.name],\n            detail: `Credential '${credType}' on node '${n.name}' looks like an unfilled placeholder ID.`\n          });\n        }\n      });\n    }\n  });\n\n  // 5. Deprecated node types\n  const deprecatedFound = nodes.filter(n => deprecatedTypes.includes(n.type));\n  if (deprecatedFound.length > 0) {\n    findings.push({\n      type: \"deprecated_node\",\n      severity: \"low\",\n      nodes: deprecatedFound.map(n => n.name),\n      detail: \"Node(s) use a deprecated node type that may be removed in future n8n versions.\"\n    });\n  }\n\n  // 6. Code nodes with no input validation (heuristic: no throw / no try-catch in jsCode)\n  const riskyCodeNodes = nodes.filter(n =>\n    n.type === \"n8n-nodes-base.code\" &&\n    typeof n.parameters?.jsCode === \"string\" &&\n    !/throw|try\\s*{/.test(n.parameters.jsCode)\n  );\n  if (riskyCodeNodes.length > 0) {\n    findings.push({\n      type: \"unvalidated_code_node\",\n      severity: \"low\",\n      nodes: riskyCodeNodes.map(n => n.name),\n      detail: \"Code node(s) have no throw/try-catch, so malformed input may fail silently or produce undefined values downstream.\"\n    });\n  }\n\n  // 7. Missing documentation (no sticky notes at all)\n  const hasStickyNotes = nodes.some(n => n.type === \"n8n-nodes-base.stickyNote\");\n  if (!hasStickyNotes) {\n    findings.push({\n      type: \"missing_documentation\",\n      severity: \"low\",\n      nodes: [],\n      detail: \"Workflow contains no sticky notes documenting purpose, setup, or node-level context.\"\n    });\n  }\n\n  // ---- Metrics ----\n  const metrics = {\n    nodeCount: nodes.length,\n    connectionCount: sourceNames.reduce((sum, src) => {\n      const connectionTypes = connections[src] ?? {};\n      return sum + Object.values(connectionTypes).reduce(\n        (typeSum, groups) => typeSum + (groups ?? []).reduce((s, g) => s + (g ?? []).length, 0),\n        0\n      );\n    }, 0),\n    triggerCount: nodes.filter(n => triggerTypes.includes(n.type) || /trigger/i.test(n.type)).length,\n    credentialBearingNodeTypes: Array.from(credentialBearingTypes),\n    aiNodeCount: nodes.filter(n => n.type.startsWith(\"@n8n/n8n-nodes-langchain\")).length,\n    orphanNodeCount: orphanNodes.length,\n    deadEndNodeCount: deadEndNodes.length,\n    deprecatedNodeCount: deprecatedFound.length,\n    findingCount: findings.length\n  };\n\n  return [{\n    json: {\n      success: true,\n      workflow: wf,\n      metrics,\n      findings\n    }\n  }];\n\n} catch (err) {\n  return [{\n    json: {\n      success: false,\n      error: err.message\n    }\n  }];\n}\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d1f84d66-a15f-4c1f-b2c2-5160f8fb9359",
      "name": "Risk Assessment Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1296,
        112
      ],
      "parameters": {
        "text": "=You are a Senior n8n Automation Engineer.\n\nThe workflow has already been analyzed.\n\nDO NOT invent issues.\n\nUse the findings below.\n\nExplain\n\n\u2022 Why it matters\n\n\u2022 Risk\n\n\u2022 Business impact\n\n\u2022 Best practice\n\nReturn ONLY JSON.\n\nMetrics\n\n{{ JSON.stringify($json.metrics,null,2) }}\n\nFindings\n\n{{ JSON.stringify($json.findings,null,2) }}",
        "options": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3.1
    },
    {
      "id": "60b18dff-8fe5-4a4a-a93b-6c22685b8ebd",
      "name": "Parse Structured Output",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1440,
        336
      ],
      "parameters": {
        "jsonSchemaExample": "{\n  \"score\": 0,\n  \"security\": [],\n  \"performance\": [],\n  \"cost\": \"\",\n  \"complexity\": \"\",\n  \"suggestions\": []\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "549f7161-06c0-485c-ad51-8fbadf60bc80",
      "name": "Groq Analysis Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        1296,
        336
      ],
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {}
      },
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c00ea3ca-c8d4-465c-877f-6e25d3c9e405",
      "name": "If Validation Succeeded",
      "type": "n8n-nodes-base.if",
      "position": [
        976,
        352
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e7cb25c3-e64d-4ba0-8e77-e045a7df22d8",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.success }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "6e68d559-6b72-4a8a-a1d2-85af7f043803",
      "name": "Generate Error Response",
      "type": "n8n-nodes-base.code",
      "position": [
        1360,
        720
      ],
      "parameters": {
        "jsCode": "return [{\n    json: {\n        success: false,\n        error: $json.error ?? \"Unknown validation error\"\n    }\n}];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "c2f95552-b2d2-40a9-bdaa-ad790095da14",
  "connections": {
    "Risk Assessment Agent": {
      "main": [
        [
          {
            "node": "Send Analysis Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Workflow Data": {
      "main": [
        [
          {
            "node": "If Validation Succeeded",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Workflow Received": {
      "main": [
        [
          {
            "node": "Validate Workflow Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Error Response": {
      "main": [
        [
          {
            "node": "Send Analysis Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Validation Succeeded": {
      "main": [
        [
          {
            "node": "Risk Assessment Agent",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Structured Output": {
      "ai_outputParser": [
        [
          {
            "node": "Risk Assessment Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Groq Analysis Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Risk Assessment Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  }
}