{
  "id": "loypsWkof4yBWa0v",
  "name": "AI-Powered Vendor Security Risk Analyzer",
  "tags": [],
  "nodes": [
    {
      "id": "7885b6b9-eb0c-4223-94c4-729582119263",
      "name": "Add required fields",
      "type": "n8n-nodes-base.set",
      "position": [
        992,
        2304
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "6060b306-5ddc-4a44-8e4a-7c6ca9b4661b",
              "name": "output.depth",
              "type": "string",
              "value": "=standard"
            },
            {
              "id": "a419cb3e-ddef-4bc7-8c8f-0fa5e82b910e",
              "name": "output.max_sources",
              "type": "string",
              "value": "30"
            },
            {
              "id": "dac1b779-2a27-4a7f-be3e-adeafcf0f61e",
              "name": "output.timeout_seconds",
              "type": "string",
              "value": "900"
            },
            {
              "id": "3ecb48a7-1f18-46ef-9253-073899b95673",
              "name": "output.hints",
              "type": "array",
              "value": "[\"SOC2\", \"privacy policy\"]"
            },
            {
              "id": "31d628d2-6b73-4eba-b6a3-48ed320ae1ef",
              "name": "output.jira_key",
              "type": "string",
              "value": "={{ $('Jira task webhook').item.json.body.issue.key }}"
            },
            {
              "id": "9938bcaf-061a-4013-8a6b-9e7718df32d4",
              "name": "output.effectiveQuery",
              "type": "string",
              "value": "={{\n  (() => {\n    const company = ($json.output.companyName ?? '').toString().trim();\n    const product = ($json.output.productName ?? '').toString().trim();\n\n    if (!company && !product) return '';\n    if (!product) return company;\n\n    if (company.toLowerCase() === product.toLowerCase()) return company;\n    return (company + ' ' + product).trim();\n  })()\n}}"
            },
            {
              "id": "70e59f62-a557-447b-8f78-a4ab2d770f85",
              "name": "output.companyUrl_norm",
              "type": "string",
              "value": "={{   (() => {     const u = ($json.output.companyUrl ?? '').toString().trim();     if (!u) return null;     if (/^https?:\\/\\//i.test(u)) return u;     return 'https://' + u;   })() }}"
            },
            {
              "id": "a418b87b-f726-4c6e-ad2e-a3a13d05e4c3",
              "name": "output.domain",
              "type": "string",
              "value": "={{\n  (() => {\n    const u = (($json.output.companyUrl_norm ?? $json.output.companyUrl) ?? '').toString().trim();\n    if (!u) return null;\n\n    const withScheme = /^https?:\\/\\//i.test(u) ? u : ('https://' + u);\n\n    try {\n      const host = new URL(withScheme).hostname.toLowerCase();\n      return host.replace(/^www\\./, '');\n    } catch (e) {\n      return u\n        .toLowerCase()\n        .replace(/^https?:\\/\\//, '')\n        .replace(/\\/.*$/, '')\n        .replace(/^www\\./, '')\n        .trim() || null;\n    }\n  })()\n}}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "1a7a4121-327b-4053-aaed-428105010316",
      "name": "Extract vendor identity",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        416,
        2304
      ],
      "parameters": {
        "text": "=You are a precise entity extractor for SAAS due-diligence automation. \nAnalyze the incoming JSON payload from Jira webhook and extract exactly three fields: companyName, companyUrl, productName.\n{{ JSON.stringify($json) }}\n\nPriority rules (follow strictly in order):\n1. Find the vendor URL first \u2014 this is the primary source:\n   - Prefer explicit URLs in any field: customfield_*, description, summary, changelog, or body.\n   - Especially look for fields like customfield_12270 (often \"Vendor URL\" or \"Website\").\n   - If URL found (e.g., \"https://www.clay.com/\" or \"clay.com\"), use it as companyUrl.\n   - Normalize: add https:// if missing, strip path/query \u2192 only origin (e.g., https://www.clay.com/ \u2192 https://www.clay.com).\n\n2. companyName:\n   - Derive from companyUrl root domain: \n     - clay.com \u2192 \"Clay\"\n     - upstash.com \u2192 \"Upstash\"\n     - chatgpt.com \u2192 \"ChatGPT\"\n     - acme.com \u2192 \"Acme\"\n   - Remove \"www.\", capitalize properly.\n   - If explicit company name exists in description/summary and differs from domain-derived, prefer explicit only if it's clearly the vendor (e.g., \"OpenAI\" for chatgpt.com).\n   - Do NOT use issue summary as companyName unless no URL found.\n\n3. productName:\n   - Prefer explicit product mention near URL or in summary/description.\n   - If summary = \"Clay\" and URL = \"https://www.clay.com/\", then productName = \"Clay\" (same as company).\n   - If summary = \"ChatGPT Atlas\" and URL = \"https://chatgpt.com/atlas/\", then productName = \"ChatGPT Atlas\".\n   - If path in URL suggests product (e.g., /atlas, /redis), use it only if no better name.\n   - If no distinction, productName can equal companyName.\n\n4. Fallbacks:\n   - If no URL \u2192 try headers.host (e.g., your-instance.app.n8n.cloud \u2192 ignore, not vendor).\n   - If still no URL \u2192 companyUrl = null.\n   - If no clear name \u2192 companyName = null.\n\n5. Output ONLY valid JSON \u2014 no text, no explanations:\n{\n  \"companyName\": string | null,\n  \"companyUrl\": string | null,\n  \"productName\": string | null,\n  \"source\": {\n    \"companyName\": \"brief description or JSON path\",\n    \"companyUrl\": \"brief description or JSON path\",\n    \"productName\": \"brief description or JSON path\"\n  }\n}\n\nExamples:\n- URL: \"https://www.clay.com/\", summary: \"Clay\" \u2192 \n  companyName: \"Clay\", companyUrl: \"https://www.clay.com\", productName: \"Clay\", source: {companyUrl: \"customfield_12270\", ...}\n\n- URL: \"https://chatgpt.com/atlas/\", summary: \"ChatGPT Atlas\" \u2192 \n  companyName: \"ChatGPT\", companyUrl: \"https://chatgpt.com\", productName: \"ChatGPT Atlas\"\n\nNow analyze the provided JSON payload and return the JSON object.",
        "options": {
          "systemMessage": "You are the Orchestrator agent for an evidence-driven security research pipeline. \nYour role: Accept input payload {requestId, companyName, url, depth, max_sources, timeout_seconds, hints (optional), cache_key (optional), productName (optional)}. \n\nNormalize inputs:\n  \u2022 Add https:// to url if missing.\n  \u2022 Generate cache_key = sha256(companyName + url + (productName or \"\")) if not provided.\n  \u2022 Compute effectiveQuery = productName ? \"{{companyName}} {{productName}}\" : \"{{companyName}}\".\n\nCheck cache for fresh results (TTL=7 days; if cached and valid, return it immediately).\n\nDispatch 7 Worker agents in parallel (compliance, data-handling, privacy, security-controls, availability, pricing, company-intel), each with:\n  \u2022 Full payload\n  \u2022 effectiveQuery\n  \u2022 Section-specific instructions\n\nAggregate responses while preserving all evidence:\n  \u2022 Use Chain-of-Thought to detect contradictions (company vs. product vs. third-party).\n  \u2022 Compute confidence per rules.\n  \u2022 Derive data_categories and data_sensitivity (product-aware mapping).\n  \u2022 Flag missing keys in missing_info.\n\nEnforce timeouts by monitoring duration_ms; retry once on transient errors.\nPersist evidence snapshots locally or via cache.\nPrioritize official sources (domain matches url) \u2014 if productName set, prioritize product-specific pages (e.g., plugin docs, GitHub, API portal).\n\nUse deterministic behavior (temperature=0.0).\nOutput ONLY the final JSON matching the schema \u2014 no free text, explanations, or extra keys.\nIf aggregation fails, include error in status field."
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "7013b14a-3d9a-4c88-aeae-940fb366aaff",
      "name": "Process values",
      "type": "n8n-nodes-base.set",
      "position": [
        768,
        2304
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "b8a5cbb8-a870-4b8a-adf7-b3e8409db3db",
              "name": "output",
              "type": "object",
              "value": "={{ $json.output }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ef53a249-680b-4298-8072-4aabd9fac653",
      "name": "Grok 4 Fast (extractor)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        416,
        2496
      ],
      "parameters": {
        "model": "x-ai/grok-4-fast",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "4a473584-e9cb-4c37-9cc1-b6bb520747f3",
      "name": "Discover seed URLs",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1328,
        2304
      ],
      "parameters": {
        "text": "=Analyze this vendor:\n- Company Name: {{ $json.output.companyName }}\n- Input URL: {{ $json.output.companyUrl_norm }}\n- Product Name: {{ $json.output.productName }}\n\nINSTRUCTIONS:\n1. Call `perplexity_sub_agent` tool with query: \"[Company Name] official website trust center security compliance\".\n2. Call `perplexity_sub_agent` tool with query: \"site:[Input Domain] OR site:[Company Name] privacy policy terms dpa\".\n3. ANALYZE IDENTITY:\n   - Does the search result redirect to a different domain? (e.g., input `manus.im` -> result `manus.ai`)?\n   - If yes, treat the redirect target as the `primary_domain`.\n4. EXTRACT URLS:\n   - Map found URLs to the output schema.\n   - For 'trust_portal', look for subdomains like `trust.`, `security.`, or paths like `/security`, `/compliance`.",
        "options": {
          "systemMessage": "=ROLE: Seed Discovery & Identity Verification Agent\n\nGOAL:\nYou are the first step in a security due-diligence pipeline. Your job is to:\n1. Identify the \"Official Truth\" domains for the company.\n2. Handle Domain Aliases (e.g., if input is `manus.im` but real site is `manus.ai`, accept the primary).\n3. Return a structured map of URLs for downstream workers.\n\nTOOL USAGE (MANDATORY):\n- You are an OFFLINE model. You MUST use `perplexity_sub_agent` to verify links.\n- Do NOT guess URLs. Only return URLs found via the tool.\n\nOUTPUT RULES (CRITICAL):\n1. **RAW JSON ONLY:** Do NOT use Markdown code blocks (no ```json).\n2. **NO CHATTER:** Do NOT add an \"Analysis Summary\", \"Note\", or any text before/after the JSON.\n3. Start output with `{` and end with `}`.\n\nOUTPUT SCHEMA:\n{\n  \"status\": \"ok\",\n  \"identity\": {\n    \"input_domain\": \"...\",\n    \"primary_domain\": \"...\",\n    \"is_alias\": boolean\n  },\n  \"seed_urls\": {\n    \"trust_portal\": \"url or null\",\n    \"security_page\": \"url or null\",\n    \"privacy_policy\": \"url or null\",\n    \"terms\": \"url or null\",\n    \"subprocessors\": \"url or null\",\n    \"status_page\": \"url or null\"\n  }\n}",
          "returnIntermediateSteps": true
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3
    },
    {
      "id": "f402317a-7041-4a44-810e-75fb464cc737",
      "name": "Compliance Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2400,
        1264
      ],
      "parameters": {
        "text": "=TARGET: {{ $('Add required fields').item.json.output.companyName }}\nPRIMARY DOMAIN: {{ $json.output.identity.primary_domain }}\nSEED URLS: {{ JSON.stringify($json.output.seed_urls) }}\n\nTASK:\nUsing the seed URLs (specifically 'trust_portal' and 'security_page') and the `perplexity_sub_agent`:\n1. Find evidence of SOC 2 (Type I or II), ISO 27001, PCI DSS, FedRAMP or any other.\n2. Check the dates. Are the certs current?\n3. If no direct PDF is found, look for a \"Request Access\" page (common for SOC2).\n\nTARGET: {{ $('Add required fields').item.json.output.companyName }}\nPRIMARY DOMAIN: {{ $json.output.identity.primary_domain }}\nSEED URLS: {{ JSON.stringify($json.output.seed_urls) }}\n\nTASK:\n1. **TRUST CENTER DEEP DIVE (Priority 1):**\n   - If a 'trust_portal' URL is provided (e.g., trust.manus.im), you MUST analyze it specifically.\n   - Trust Centers often host \"Subprocessors\", \"Security Controls\", and \"Report Request\" buttons.\n   - Search specifically for: `site:{{ $json.output.seed_urls.trust_portal }} \"SOC 2\" OR \"ISO 27001\" OR \"controls\" OR \"subprocessors\"`\n   - If the main trust page seems empty, look for sub-pages like `/compliance`, `/security`, `/legal`.\n\n2. **CERTIFICATION VERIFICATION:**\n   - Find evidence of SOC 2 (Type I or II), ISO 27001, PCI DSS, or FedRAMP.\n   - **Crucial:** Distinguish between \"We are SOC 2 Compliant\" (marketing claim) vs \"Download SOC 2 Report\" (evidence).\n   - Check dates: Evidence older than 18 months is \"Outdated\".\n\n3. **GAPS:**\n   - If you found the Trust Center but cannot see the *content* of the reports (e.g., locked behind login), Report \"Status: Gated/Requestable\" rather than \"Missing\".",
        "options": {
          "maxIterations": 20,
          "systemMessage": "ROLE: Compliance Research Specialist\nGOAL: Verify formal security attestations (SOC 2, ISO, PCI, FedRAMP).\n\n## TOOL USAGE (MANDATORY)\n- You are an OFFLINE model. You MUST use `perplexity_sub_agent`.\n- You MUST perform a specific \"Deep Dive\" search if a Trust Portal URL is provided.\n\n## OUTPUT RULES\n1. **RAW JSON ONLY:** No Markdown, no chatter.\n2. **STATUS DEFINITIONS:**\n   - \"Present\": Found direct evidence (PDF, badge, text).\n   - \"Gated\": Found a \"Request Report\" or \"NDA\" page.\n   - \"Missing\": No evidence found after searching.\n3. **EVIDENCE:** Include exact URLs and concise snippets.\n\nOUTPUT SCHEMA:\n{\n  \"agent\": \"Compliance Agent\",\n  \"key_findings\": [\n    {\n      \"topic\": \"SOC 2|ISO 27001|PCI DSS|FedRAMP|HIPAA\",\n      \"status\": \"Present|Missing|Gated|Outdated\",\n      \"summary\": \"e.g. 'SOC 2 Type II report covering 2024-2025 available via Trust Portal.'\",\n      \"url\": \"...\"\n    }\n  ],\n  \"evidence\": [ { \"url\": \"...\", \"snippet\": \"...\", \"source_type\": \"official|third_party\" } ]\n}"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "532f2d05-49c1-41a8-90b2-d57d4febe0d2",
      "name": "Data Handling Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2400,
        1424
      ],
      "parameters": {
        "text": "=TARGET: {{ $('Add required fields').item.json.output.companyName }}\nPRIMARY DOMAIN: {{ $json.output.identity.primary_domain }}\nSEED URLS: {{ JSON.stringify($json.output.seed_urls) }}\n\nTASK:\n1. **SUBPROCESSORS:**\n   - Search: `site:{{ $json.output.identity.primary_domain }} \"subprocessors\" OR \"sub-processors\" OR \"third party vendors\"`\n   - Summary: Do they have a public list?\n\n2. **DPA & RESIDENCY:**\n   - Search: `site:{{ $json.output.identity.primary_domain }} \"Data Processing Agreement\" OR \"DPA\" OR \"EU hosting\" OR \"data residency\"`\n   - Summary: Can customers choose EU vs US storage? Is there a standard DPA?\n\n3. **RETENTION:**\n   - Search: `site:{{ $json.output.identity.primary_domain }} \"data retention\" OR \"deletion policy\" OR \"termination\"`\n   - Summary: What happens to data after the contract ends? (e.g., deleted in 30 days).",
        "options": {
          "maxIterations": 20,
          "systemMessage": "ROLE: Data Governance Specialist\nGOAL: Map data processing, residency, subprocessors, and retention policies.\n\n## TOOL USAGE (MANDATORY)\n- You are an OFFLINE model. Use `perplexity_sub_agent`.\n\n## OUTPUT RULES\n1. **RAW JSON ONLY:** No Markdown.\n2. **PRIORITY:** Focus on finding the actual DPA PDF or page.\n\nOUTPUT SCHEMA:\n{\n  \"agent\": \"Data Handling Agent\",\n  \"key_findings\": [\n    {\n      \"topic\": \"DPA|Subprocessors|Data Residency|Retention\",\n      \"status\": \"Present|Missing|Unclear\",\n      \"summary\": \"...\",\n      \"url\": \"...\"\n    }\n  ],\n  \"evidence\": [ { \"url\": \"...\", \"snippet\": \"...\", \"source_type\": \"official|third_party\" } ]\n}"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "dffdda53-e4ef-47e3-b7ec-0558340ea580",
      "name": "Privacy & Legal Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2400,
        1616
      ],
      "parameters": {
        "text": "=TARGET: {{ $('Add required fields').item.json.output.companyName }}\nPRIMARY DOMAIN: {{ $json.output.identity.primary_domain }}\nSEED URLS: {{ JSON.stringify($json.output.seed_urls) }}\n\nTASK:\n1. **FRAMEWORKS:**\n   - Search: `site:{{ $json.output.identity.primary_domain }} \"GDPR\" OR \"CCPA\" OR \"CPRA\"`\n   - Search: `site:{{ $json.output.identity.primary_domain }}\"Data Privacy Framework\" OR \"Standard Contractual Clauses\" OR \"SCC\"`\n\n2. **USER RIGHTS:**\n   - Check the Privacy Policy URL provided.\n   - Summary: Do they list specific rights (Right to be Forgotten, Access)? Is there a `privacy@` email?",
        "options": {
          "maxIterations": 20,
          "systemMessage": "ROLE: Privacy Legal Analyst\nGOAL: Assess alignment with GDPR, CCPA, and International Data Transfers.\n\n## TOOL USAGE (MANDATORY)\n- You are an OFFLINE model. Use `perplexity_sub_agent`.\n\n## OUTPUT RULES\n1. **RAW JSON ONLY:** No Markdown.\n2. **SPECIFICITY:** Do not just say \"They comply with laws\". Quote the specific frameworks (GDPR, CCPA, DPF).\n\nOUTPUT SCHEMA:\n{\n  \"agent\": \"Privacy Agent\",\n  \"key_findings\": [\n    {\n      \"topic\": \"GDPR|CCPA|Transfers|Rights\",\n      \"status\": \"Explicit|Implied|Missing\",\n      \"summary\": \"...\",\n      \"url\": \"...\"\n    }\n  ],\n  \"evidence\": [ { \"url\": \"...\", \"snippet\": \"...\", \"source_type\": \"official|third_party\" } ]\n}"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "a234a10b-4597-4f5d-9ea5-4e5e4108241f",
      "name": "Security Controls Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2400,
        1808
      ],
      "parameters": {
        "text": "=TARGET: {{ $('Add required fields').item.json.output.companyName }}\nPRIMARY DOMAIN: {{ $json.output.identity.primary_domain }}\nSEED URLS: {{ JSON.stringify($json.output.seed_urls) }}\n\nTASK:\n1. **AUTHENTICATION:**\n   - Search: `site:{{ $json.output.identity.primary_domain }} \"SSO\" OR \"SAML\" OR \"SCIM\" OR \"Single Sign On\"`\n   - Search: `site:{{ $json.output.identity.primary_domain }} \"MFA\" OR \"2FA\" OR \"Multi-factor\"`\n\n2. **RED TEAM / INCIDENT HISTORY (CRITICAL):**\n   - Search: `\"{{ $('Process values').item.json.output.companyName }}\" data breach OR security incident OR ransomware OR hacked`\n   - Search: `\"{{ $('Process values').item.json.output.companyName }}\" CVE vulnerabilities exploit`\n   - Search: `site:reddit.com \"{{ $('Process values').item.json.output.companyName }}\" security concern`\n\n3. **VULNERABILITY DISCLOSURE:**\n   - Search: `site:{{ $json.output.identity.primary_domain }} \"security.txt\" OR \"bug bounty\" OR \"responsible disclosure\"`\n   - Search: `\"{{ $('Process values').item.json.output.companyName }}\" bug bounty program`\n",
        "options": {
          "maxIterations": 20,
          "systemMessage": "ROLE: Technical Security Analyst\nGOAL: Validate existence of SSO, MFA, RBAC, and Vulnerability Disclosure.\n\n## TOOL USAGE (MANDATORY)\n- You are an OFFLINE model. Use `perplexity_sub_agent`.\n\n## OUTPUT RULES\n1. **RAW JSON ONLY:** No Markdown.\n2. **FOCUS:** Differentiate between \"Internal security\" (what they do) and \"Customer features\" (SSO/MFA for users). Focus on Customer Features.\n\nOUTPUT SCHEMA:\n{\n  \"agent\": \"Security Controls Agent\",\n  \"key_findings\": [\n    {\n      \"topic\": \"SSO|MFA|RBAC|VDP\",\n      \"status\": \"Supported|Not Found|Add-on\",\n      \"summary\": \"e.g. 'SAML SSO supported via Okta/OneLogin'\",\n      \"url\": \"...\"\n    }\n  ],\n  \"evidence\": [ { \"url\": \"...\", \"snippet\": \"...\", \"source_type\": \"official|third_party\" } ]\n}"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "9fb288fb-85e1-4052-9789-94809cf39b31",
      "name": "Availability Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2400,
        2016
      ],
      "parameters": {
        "text": "=TARGET: {{ $('Add required fields').item.json.output.companyName }}\nPRIMARY DOMAIN: {{ $json.output.identity.primary_domain }}\nSEED URLS: {{ JSON.stringify($json.output.seed_urls) }}\n\nTASK:\n1. **STATUS PAGE:**\n   - If a status URL exists, check it. Does it show historical uptime?\n   - If not, search: `site:{{ $json.output.identity.primary_domain }} \"status\" OR \"uptime\"`\n   - Search broad: `\"{{ $('Process values').item.json.output.companyName }}\" status page`\n\n2. **SLA & DR:**\n   - Search: `site:{{ $json.output.identity.primary_domain }} \"SLA\" OR \"Service Level Agreement\" OR \"uptime guarantee\"`\n   - Search: `site:{{ $json.output.identity.primary_domain }}\"disaster recovery\" OR \"RTO\" OR \"RPO\"`",
        "options": {
          "maxIterations": 20,
          "systemMessage": "ROLE: Site Reliability Analyst\nGOAL: Confirm uptime transparency, Status Pages, and SLA terms.\n\n## TOOL USAGE (MANDATORY)\n- You are an OFFLINE model. Use `perplexity_sub_agent`.\n\n## OUTPUT RULES\n1. **RAW JSON ONLY:** No Markdown.\n\nOUTPUT SCHEMA:\n{\n  \"agent\": \"Availability Agent\",\n  \"key_findings\": [\n    {\n      \"topic\": \"Status Page|SLA|Disaster Recovery\",\n      \"status\": \"Public|Private|Missing\",\n      \"summary\": \"...\",\n      \"url\": \"...\"\n    }\n  ],\n  \"evidence\": [ { \"url\": \"...\", \"snippet\": \"...\", \"source_type\": \"official|third_party\" } ]\n}"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "0044fad7-5069-46b0-afe5-68351726b27c",
      "name": "Pricing Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2400,
        2224
      ],
      "parameters": {
        "text": "=TARGET: {{ $('Add required fields').item.json.output.companyName }}\nPRIMARY DOMAIN: {{ $json.output.identity.primary_domain }}\n\nTASK:\n1. **TIERS & SSO TAX:**\n   - Search: `site:{{ $json.output.identity.primary_domain }}pricing`\n   - Search: `site:{{ $json.output.identity.primary_domain }}\"enterprise plan\" features`\n   - **Specific Goal:** Does the \"Pro\" or \"Team\" plan include SSO/SAML? Or is it Enterprise only?\n\n2. **COMMERCIAL TERMS:**\n   - Search: `site:{{ $json.output.identity.primary_domain }}\"refund policy\" OR \"cancellation\" OR \"money back\"`",
        "options": {
          "maxIterations": 20,
          "systemMessage": "ROLE: Procurement Analyst & JSON API\nGOAL: Extract commercial terms, pricing tiers, and \"SSO Tax\" risks.\n\n## TOOL USAGE (MANDATORY)\n- You are an OFFLINE model. Use `perplexity_sub_agent`.\n\n## OUTPUT RULES (CRITICAL)\n1. **YOU ARE AN API.** Do not speak plain English. Do not write summaries.\n2. **RAW JSON ONLY.** No Markdown fences. Start with `{` and end with `}`.\n3. If SSO costs extra (e.g., \"Enterprise only\" or \"Flat fee\"), mark \"SSO Tax\" as \"Verified\".\n\nOUTPUT SCHEMA:\n{\n  \"agent\": \"Pricing Agent\",\n  \"key_findings\": [\n    {\n      \"topic\": \"Pricing Structure|SSO Tax|Refunds\",\n      \"status\": \"Verified|Unclear\",\n      \"summary\": \"Brief factual sentence.\",\n      \"url\": \"...\"\n    }\n  ],\n  \"evidence\": [ { \"url\": \"...\", \"snippet\": \"...\", \"source_type\": \"official|third_party\" } ]\n}"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "e9405ad0-df40-4fd5-876e-0f7e3d0227fb",
      "name": "Company Intel Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2400,
        2448
      ],
      "parameters": {
        "text": "=TARGET: {{ $('Add required fields').item.json.output.companyName }}\nPRIMARY DOMAIN: {{ $json.output.identity.primary_domain }}\n\nTASK:\n1. **CORPORATE IDENTITY:**\n   - Search: `\"{{ $('Process values').item.json.output.companyName }}\" legal entity name headquarters`\n   - Search: `\"{{ $('Process values').item.json.output.companyName }}\" funding crunchbase linkedin`\n\n2. **VERIFY:**\n   - Official Legal Name (e.g., \"Manus AI, Inc.\").\n   - Headquarters Country/City.\n   - Funding stage (Seed, Series A, Public, Acquired?).\n   - Year Founded.",
        "options": {
          "maxIterations": 20,
          "systemMessage": "ROLE: Corporate Intelligence Analyst\nGOAL: Verify Legal Entity, HQ, Funding, and Acquisition status.\n\n## TOOL USAGE (MANDATORY)\n- You are an OFFLINE model. Use `perplexity_sub_agent`.\n\n## OUTPUT RULES\n1. **RAW JSON ONLY:** No Markdown.\n\nOUTPUT SCHEMA:\n{\n  \"agent\": \"Company Intel Agent\",\n  \"key_findings\": [\n    {\n      \"topic\": \"Legal Entity|HQ|Funding|Founded\",\n      \"status\": \"Verified|Unknown\",\n      \"summary\": \"...\",\n      \"url\": \"...\"\n    }\n  ],\n  \"evidence\": [ { \"url\": \"...\", \"snippet\": \"...\", \"source_type\": \"official|third_party\" } ]\n}"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "8fd84ede-806d-4f32-83c3-d7ac0a6e721e",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        3776,
        1936
      ],
      "parameters": {
        "options": {
          "mergeLists": true
        },
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "output"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2f5034a6-03df-4852-b0b2-4c85435dff23",
      "name": "Lead Security Analyst",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        4064,
        1936
      ],
      "parameters": {
        "text": "=Here are the structured findings from the research team:\n{{ JSON.stringify($('Aggregate').item.json.output) }}\n\nJira Key: {{ $('Jira task webhook').item.json.body.issue.key }}\nJira Browser URL: https://YOUR-JIRA-DOMAIN.atlassian.net/browse/{{ $('Jira task webhook').item.json.body.issue.key }}\n\nTASK:\n1. Deduplicate the \"evidence\" across all agents.\n2. Generate the Jira Wiki Markup body.\n3. Generate the Slack Block Kit JSON (including the \"Actions\" button).",
        "options": {
          "systemMessage": "ROLE: Lead Security Analyst (Aggregator)\nGOAL: Synthesize 7 research streams into a unified Vendor Security Report for Jira and Slack.\n\nINPUT DATA:\n- Research results from 7 agents.\n- Jira Key and URL.\n\nINSTRUCTIONS:\n1. **Consolidate Evidence:**\n   - Merge duplicate URLs.\n   - Assign global IDs (E1, E2...).\n   - Reference IDs in findings.\n\n2. **Assess Risk:**\n   - HIGH: Missing SOC2/ISO, No DPA, No SSO, HIPAA violations.\n   - MED: No Status Page, Missing Subprocessor list.\n\n3. **Generate Output JSON:**\n   - `jira_body`: Jira Wiki Markup (h1, h2, tables).\n   - `slack_blocks`: Native JSON array.\n     - Block 1: Header (\"Vendor Security Report: <Vendor>\")\n     - Block 2: Section (Context/Overview)\n     - Block 3: Section (Highlights - Top 3 Pros)\n     - Block 4: Section (Risks - Top 3 Gaps)\n     - Block 5: Actions (Button: \"View Report in Jira\", URL: <Jira Browser URL>)\n\nOUTPUT JSON SCHEMA:\n{\n  \"jira_body\": \"string (wiki markup)\",\n  \"slack_blocks\": [\n    { \"type\": \"header\", \"...\":\"...\" },\n    { \"type\": \"section\", \"...\":\"...\" },\n    { \"type\": \"actions\", \"elements\": [ { \"type\": \"button\", \"text\": {\"...\":\"...\"}, \"url\": \"...\" } ] }\n  ],\n  \"slack_text\": \"Short summary for notification\"\n}",
          "returnIntermediateSteps": false
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "11daaab5-350f-4a41-bcd7-fafc58faf50e",
      "name": "Post to Slack channel",
      "type": "n8n-nodes-base.slack",
      "position": [
        4832,
        1936
      ],
      "parameters": {
        "select": "channel",
        "blocksUi": "={\n  \"attachments\": [\n    {\n      \"color\": \"{{ $json.colorBar || '#D70000' }}\",\n      \"blocks\": {{ JSON.stringify($('Unwrap final report').item.json.output.slack_blocks) }}\n    }\n  ]\n}",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": ""
        },
        "messageType": "block",
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "1626ba6b-a91a-4341-9680-46cae20f2f4e",
      "name": "perplexity_sub_agent",
      "type": "@n8n/n8n-nodes-langchain.agentTool",
      "position": [
        2688,
        2784
      ],
      "parameters": {
        "text": "=Research the following query passed by the lead agent:\n\"{{ $fromAI('query') }}\"\n\nCONSTRAINTS:\n1. Max Results: {{ $fromAI('max_sources') || 6 }}\n2. Format: Return STRICT JSON only.\n3. Content: Focus on extracting facts, dates, and snippets.",
        "options": {
          "systemMessage": "=ROLE: Web Search & Extraction Sub-Agent\n\nGOAL:\nYou are an interface to the real-time web. You receive a specific `query` from a Lead Researcher and must return factual results in JSON format.\n\nINPUT HANDLING:\n- You will receive a query variable. You MUST run a live web search for it.\n- If the query contains `site:`, strictly filter results to that domain.\n\nOUTPUT RULES (STRICT JSON):\n1. Do NOT chat. Do NOT use Markdown (```json). Just return the raw JSON object.\n2. Structure:\n{\n  \"results\": [\n    {\n      \"title\": \"Page Title\",\n      \"url\": \"Full URL\",\n      \"date\": \"YYYY-MM-DD\" or null,\n      \"snippet\": \"The specific text relevant to the query.\",\n      \"source_type\": \"first_party (if matches site: operator) | third_party (reviews/news)\"\n    }\n  ]\n}\n\n3. SNIPPET QUALITY:\n   - If the query asks for \"SOC 2\", the snippet MUST contain the text regarding SOC 2.\n   - Do not return generic homepage descriptions.\n\nERROR HANDLING:\n- If no results found, return { \"results\": [] }."
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d165c192-ce2a-4316-8550-55b1991d07eb",
      "name": "Parse discovery JSON",
      "type": "n8n-nodes-base.code",
      "position": [
        1680,
        2304
      ],
      "parameters": {
        "jsCode": "// Clean up markdown/text from the AI output\nconst raw = items[0].json.output;\nlet clean = raw;\n\n// Remove markdown fences\nif (typeof raw === 'string') {\n  clean = raw.replace(/```json/g, '').replace(/```/g, '');\n  \n  // Extract just the JSON object (find first { and last })\n  const firstOpen = clean.indexOf('{');\n  const lastClose = clean.lastIndexOf('}');\n  if (firstOpen !== -1 && lastClose !== -1) {\n    clean = clean.substring(firstOpen, lastClose + 1);\n  }\n  \n  try {\n    return [{ json: { output: JSON.parse(clean) } }];\n  } catch (e) {\n    // If parse fails, return raw to debug\n    return [{ json: { output: raw, error: \"Parse Failed\" } }];\n  }\n}\n\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "dc9deb61-41f5-4eca-87aa-3b6269f6f245",
      "name": "Section 1 \u2014 Ingestion Pipeline",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        1888
      ],
      "parameters": {
        "width": 1152,
        "height": 784,
        "content": "## 1. Ingestion Pipeline\n\nReceive the Jira webhook and normalize the vendor target (company, URL, product) into a clean payload for downstream agents."
      },
      "typeVersion": 1
    },
    {
      "id": "94b1df9e-d5df-4848-9b82-bbe91269b1c2",
      "name": "Section 2 \u2014 Discovery Engine",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1248,
        1872
      ],
      "parameters": {
        "width": 816,
        "height": 800,
        "content": "## 2. Discovery Engine\n\nResolve the vendor's canonical URLs (Trust Center, Privacy Policy, Subprocessors, Status page) before deep research. Prevents downstream agents from hallucinating URLs."
      },
      "typeVersion": 1
    },
    {
      "id": "9a94618b-4dd0-4f88-8159-3ec0f4b4e16b",
      "name": "Claude Sonnet 4.5 (discovery)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        1328,
        2496
      ],
      "parameters": {
        "model": "anthropic/claude-sonnet-4.5",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "6d58ea4a-1dff-413a-a650-a4a8de9dfa68",
      "name": "Unwrap discovery payload",
      "type": "n8n-nodes-base.set",
      "position": [
        1888,
        2304
      ],
      "parameters": {
        "options": {
          "ignoreConversionErrors": true
        },
        "assignments": {
          "assignments": [
            {
              "id": "924c17f8-d026-480b-9553-72f927160e94",
              "name": "output",
              "type": "object",
              "value": "={{ $json.output }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "afc9b175-bcd7-4eda-9f0e-1e50da88d44d",
      "name": "Section 3 \u2014 Parallel Research Swarm",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2096,
        1024
      ],
      "parameters": {
        "width": 928,
        "height": 2048,
        "content": "## 3. Parallel Research Swarm\n\nSeven specialist agents run concurrently \u2014 Compliance, Data Handling, Privacy & Legal, Security Controls, Availability, Pricing, Company Intel \u2014 each with a focused JSON output schema. All share one `perplexity_sub_agent` tool for live web search and the same Claude Sonnet 4.5 brain."
      },
      "typeVersion": 1
    },
    {
      "id": "9d85fa50-f97a-424e-bae6-ef430b7d7eaa",
      "name": "Claude Sonnet 4.5 (workers)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        2144,
        2560
      ],
      "parameters": {
        "model": "anthropic/claude-sonnet-4.5",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "467bd8f0-b129-4939-ad86-2056d8729714",
      "name": "Section 4 \u2014 Synthesis Pipeline",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3088,
        1504
      ],
      "parameters": {
        "width": 864,
        "height": 720,
        "content": "## 4. Synthesis Pipeline\n\nCollapse the 7 asynchronous JSON streams into one clean evidence set: merge \u2192 sanitize (strip markdown fences, handle bad JSON) \u2192 unwrap \u2192 aggregate all `key_findings[]` and `evidence[]` arrays for the Lead Analyst."
      },
      "typeVersion": 1
    },
    {
      "id": "88f838db-401d-488a-b713-bd1b0d9a27c0",
      "name": "Comment on Jira ticket",
      "type": "n8n-nodes-base.jira",
      "position": [
        4624,
        1936
      ],
      "parameters": {
        "comment": "={{ $json.output.jira_body }}",
        "options": {
          "wikiMarkup": true
        },
        "issueKey": "={{ $('Jira task webhook').item.json.body.issue.key }}",
        "resource": "issueComment"
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "eaefa639-909d-46bf-866a-61d86cfd0506",
      "name": "Unwrap final report",
      "type": "n8n-nodes-base.set",
      "position": [
        4416,
        1936
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "becc0407-52ad-47eb-bbc0-14501ccd1919",
              "name": "output",
              "type": "object",
              "value": "={{ $json.output }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "2742011c-bffb-4799-aeff-57cd59120f94",
      "name": "Section 5 \u2014 Reporting Layer",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4000,
        1504
      ],
      "parameters": {
        "width": 1072,
        "height": 720,
        "content": "## 5. Reporting Layer\n\nLead Analyst deduplicates evidence, scores risk (High / Med / Low), and produces both Jira Wiki Markup and Slack Block Kit outputs \u2014 posted to the original ticket and to your security channel."
      },
      "typeVersion": 1
    },
    {
      "id": "d57ec509-f95c-42d4-bd2f-731ec6fbec19",
      "name": "Grok 4 Fast (synthesizer)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        4064,
        2080
      ],
      "parameters": {
        "model": "x-ai/grok-4-fast",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "069c4ae8-253c-4878-a076-59cb64142f1b",
      "name": "Perplexity Sonar Pro (search)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "onError": "continueRegularOutput",
      "maxTries": 5,
      "position": [
        2544,
        2912
      ],
      "parameters": {
        "model": "perplexity/sonar-pro-search",
        "options": {}
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 5000
    },
    {
      "id": "7bbdb7da-9548-4c3e-95fb-8c0967d574b1",
      "name": "Merge agent outputs",
      "type": "n8n-nodes-base.merge",
      "position": [
        3184,
        1856
      ],
      "parameters": {
        "numberInputs": 7
      },
      "typeVersion": 3.2
    },
    {
      "id": "ca8a6529-ff37-4e3b-bb3e-eb0a3a76bc2a",
      "name": "Sanitize agent JSON",
      "type": "n8n-nodes-base.code",
      "position": [
        3392,
        1936
      ],
      "parameters": {
        "jsCode": "// Loop over all 7 agent results\nconst results = items.map(item => {\n  let raw = item.json.output;\n  \n  if (typeof raw === 'string') {\n    let clean = raw.replace(/```json/g, '').replace(/```/g, '').trim();\n    const firstCurly = clean.indexOf('{');\n    const lastCurly = clean.lastIndexOf('}');\n    if (firstCurly !== -1 && lastCurly !== -1) {\n      clean = clean.substring(firstCurly, lastCurly + 1);\n    }\n    try {\n      return { json: { output: JSON.parse(clean) } };\n    } catch (e) {\n      return { json: { output: { agent: \"Error Parsing Agent\", error: \"Agent returned invalid JSON\", raw_content: raw } } };\n    }\n  }\n  return item;\n});\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "9d8e1237-a287-426d-ba02-a3d2660e012f",
      "name": "Unwrap synthesis payload",
      "type": "n8n-nodes-base.set",
      "position": [
        3568,
        1936
      ],
      "parameters": {
        "options": {
          "ignoreConversionErrors": true
        },
        "assignments": {
          "assignments": [
            {
              "id": "924c17f8-d026-480b-9553-72f927160e94",
              "name": "output",
              "type": "object",
              "value": "={{ $json.output }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "5e2cf78c-58a9-4266-bc29-10da8c5df12a",
      "name": "Jira task webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        48,
        2304
      ],
      "parameters": {
        "path": "ai-vendor-security-analyzer",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "2d1e54a5-710c-4abf-bffb-034ac838cb56",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "color": 3,
        "width": 1600,
        "height": 880,
        "content": "## \ud83d\udd10 AI-Powered Vendor Security Risk Analyzer\n\n### Who's it for\nSecurity teams, procurement, and compliance leads running third-party SaaS vendor risk assessments. If you currently spend an hour per vendor digging through Trust Centers, privacy pages, and sub-processor lists, this workflow condenses that work into ~90 seconds of automated, evidence-linked research.\n\n### What it does\nWhen a security-review ticket is opened in Jira, seven specialist research agents run in parallel across the vendor's official sources. A Lead Analyst scores the risk (High / Med / Low) and posts a consolidated report back to the Jira ticket and to a Slack channel \u2014 with direct links to every piece of evidence.\n\n### How it works\n1. **Ingest** \u2014 Jira webhook fires on ticket creation; an extractor agent parses the payload to pull the vendor's name, URL, and product.\n2. **Discover** \u2014 A Seed URL Discovery Agent resolves the canonical Trust Center, Privacy Policy, Subprocessors page and Status page. Handles domain aliases (e.g. `manus.im` \u2192 `manus.ai`).\n3. **Research (fan-out)** \u2014 Seven specialist agents run concurrently: Compliance, Data Handling, Privacy & Legal, Security Controls, Availability, Pricing, Company Intel. All share one Perplexity sub-agent for live web search.\n4. **Synthesize (fan-in)** \u2014 Merge \u2192 sanitize JSON \u2192 deduplicate evidence \u2192 aggregate into a single payload for the Lead Analyst.\n5. **Report** \u2014 Lead Analyst scores risk, formats Jira Wiki Markup + Slack Block Kit, and posts to both channels.\n\n### How to use\n1. Add three credentials: **OpenRouter** (API key), **Jira** (API token with `issueComment` permission), **Slack** (OAuth2 with `chat:write`).\n2. Wire your Jira automation rule to POST to this workflow's webhook URL.\n3. Replace every \u26a0\ufe0f placeholder called out in the green setup notes next to each relevant node (Slack `channelId`, Jira domain, vendor-URL custom field ID).\n4. Activate the workflow and open a test ticket.\n\nTypical run: ~60\u2013120s end-to-end, ~$0.03\u2013$0.08 per vendor (OpenRouter usage)."
      },
      "typeVersion": 1
    },
    {
      "id": "412a9ae2-b43b-4d59-9702-9dfcb3a5cce5",
      "name": "\u26a0\ufe0f Replace customfield_12270",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        352,
        2112
      ],
      "parameters": {
        "color": 6,
        "width": 340,
        "content": "### \u26a0\ufe0f Replace `customfield_12270`\nThis prompt references our Jira \"Vendor URL\" field ID. **Change it** to match the custom-field ID used in your Jira instance, otherwise the agent won't find the URL."
      },
      "typeVersion": 1
    },
    {
      "id": "bd3951e9-7aec-4061-866b-f0de40a55e4b",
      "name": "\u26a0\ufe0f Replace YOUR-JIRA-DOMAIN",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4016,
        1728
      ],
      "parameters": {
        "color": 6,
        "width": 324,
        "height": 180,
        "content": "### \u26a0\ufe0f Replace `YOUR-JIRA-DOMAIN`\nThe agent's prompt builds a Jira link using `YOUR-JIRA-DOMAIN.atlassian.net`. **Replace it** with your Atlassian domain (e.g. `acme.atlassian.net`), otherwise the \"View Report\" button in Slack will 404."
      },
      "typeVersion": 1
    },
    {
      "id": "2cd083ed-bead-4851-ab12-4ff6344a5299",
      "name": "\u26a0\ufe0f Set Slack channel ID",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4768,
        1728
      ],
      "parameters": {
        "color": 6,
        "width": 244,
        "height": 180,
        "content": "### \u26a0\ufe0f Set Slack `channelId`\nThe Channel ID is empty by default. **Set it** to your security channel's ID, otherwise the Slack post will fail silently."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "4b64fa22-952b-4de5-8a62-6e6573321b19",
  "connections": {
    "Aggregate": {
      "main": [
        [
          {
            "node": "Lead Security Analyst",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pricing Agent": {
      "main": [
        [
          {
            "node": "Merge agent outputs",
            "type": "main",
            "index": 5
          }
        ]
      ]
    },
    "Process values": {
      "main": [
        [
          {
            "node": "Add required fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compliance Agent": {
      "main": [
        [
          {
            "node": "Merge agent outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Jira task webhook": {
      "main": [
        [
          {
            "node": "Extract vendor identity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Availability Agent": {
      "main": [
        [
          {
            "node": "Merge agent outputs",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "Discover seed URLs": {
      "main": [
        [
          {
            "node": "Parse discovery JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add required fields": {
      "main": [
        [
          {
            "node": "Discover seed URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Company Intel Agent": {
      "main": [
        [
          {
            "node": "Merge agent outputs",
            "type": "main",
            "index": 6
          }
        ]
      ]
    },
    "Data Handling Agent": {
      "main": [
        [
          {
            "node": "Merge agent outputs",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge agent outputs": {
      "main": [
        [
          {
            "node": "Sanitize agent JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sanitize agent JSON": {
      "main": [
        [
          {
            "node": "Unwrap synthesis payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unwrap final report": {
      "main": [
        [
          {
            "node": "Comment on Jira ticket",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse discovery JSON": {
      "main": [
        [
          {
            "node": "Unwrap discovery payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "perplexity_sub_agent": {
      "ai_tool": [
        [
          {
            "node": "Discover seed URLs",
            "type": "ai_tool",
            "index": 0
          },
          {
            "node": "Compliance Agent",
            "type": "ai_tool",
            "index": 0
          },
          {
            "node": "Data Handling Agent",
            "type": "ai_tool",
            "index": 0
          },
          {
            "node": "Security Controls Agent",
            "type": "ai_tool",
            "index": 0
          },
          {
            "node": "Availability Agent",
            "type": "ai_tool",
            "index": 0
          },
          {
            "node": "Pricing Agent",
            "type": "ai_tool",
            "index": 0
          },
          {
            "node": "Company Intel Agent",
            "type": "ai_tool",
            "index": 0
          },
          {
            "node": "Privacy & Legal Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Lead Security Analyst": {
      "main": [
        [
          {
            "node": "Unwrap final report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Privacy & Legal Agent": {
      "main": [
        [
          {
            "node": "Merge agent outputs",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Comment on Jira ticket": {
      "main": [
        [
          {
            "node": "Post to Slack channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract vendor identity": {
      "main": [
        [
          {
            "node": "Process values",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Grok 4 Fast (extractor)": {
      "ai_languageModel": [
        [
          {
            "node": "Extract vendor identity",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Security Controls Agent": {
      "main": [
        [
          {
            "node": "Merge agent outputs",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Unwrap discovery payload": {
      "main": [
        [
          {
            "node": "Compliance Agent",
            "type": "main",
            "index": 0
          },
          {
            "node": "Data Handling Agent",
            "type": "main",
            "index": 0
          },
          {
            "node": "Privacy & Legal Agent",
            "type": "main",
            "index": 0
          },
          {
            "node": "Security Controls Agent",
            "type": "main",
            "index": 0
          },
          {
            "node": "Availability Agent",
            "type": "main",
            "index": 0
          },
          {
            "node": "Pricing Agent",
            "type": "main",
            "index": 0
          },
          {
            "node": "Company Intel Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unwrap synthesis payload": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Grok 4 Fast (synthesizer)": {
      "ai_languageModel": [
        [
          {
            "node": "Lead Security Analyst",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Claude Sonnet 4.5 (workers)": {
      "ai_languageModel": [
        [
          {
            "node": "Data Handling Agent",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Privacy & Legal Agent",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Security Controls Agent",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Availability Agent",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Pricing Agent",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Company Intel Agent",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Compliance Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Claude Sonnet 4.5 (discovery)": {
      "ai_languageModel": [
        [
          {
            "node": "Discover seed URLs",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Perplexity Sonar Pro (search)": {
      "ai_languageModel": [
        [
          {
            "node": "perplexity_sub_agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  }
}