{
  "name": "Scan GitHub dependency vulnerabilities with OSV.dev and alert Slack",
  "nodes": [
    {
      "parameters": {
        "content": "## Scan GitHub dependencies with OSV.dev and alert Slack\n\nFor platform and DevOps engineers who want a weekly dependency vulnerability digest without installing a scanner in every repository.\n\n### How it works\n1. Every Monday (and whenever you click Run now) the workflow reads one manifest from GitHub: package-lock.json, package.json or requirements.txt.\n2. It collects exact dependency versions (ranges in package.json are stripped to the bare number) up to your maxPackages cap.\n3. One call to the free OSV.dev querybatch API lists known advisories; each advisory is then fetched for severity and fixed versions.\n4. Findings at or above minSeverity (default HIGH) are posted to Slack. Clean runs stay silent. A missing manifest or an OSV failure stops the run (fail closed).\n\n### Setup\n1. Create a GitHub credential in n8n (personal access token with Contents: Read on the target repos) and select it on Read manifest from GitHub.\n2. Create a Slack credential and select it on Post alert to Slack, then pick a channel.\n3. Edit Settings: githubOwner, githubRepo, gitRef, manifestPath, minSeverity and maxPackages.\n4. Click Run now once to verify, then activate for the Monday schedule (timezone = the n8n instance timezone).\n\n### Customization\n- Point manifestPath at package-lock.json for exact lockfile versions.\n- Set minSeverity to MEDIUM for a stricter digest, or CRITICAL for less noise.\n- Add a GitHub Issue node after the Slack alert if you want a ticket as well as a message.",
        "height": 820,
        "width": 560
      },
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -720,
        80
      ]
    },
    {
      "parameters": {
        "content": "## 1. Triggers and settings\nSchedule Mondays 08:00 (instance timezone). Edit owner, repo, ref and manifest in Settings.",
        "height": 560,
        "width": 520,
        "color": 7
      },
      "name": "Section 1: Triggers and settings",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        80
      ]
    },
    {
      "parameters": {
        "content": "## 2. Read manifest from GitHub\nFetches one file. Missing file or auth errors stop the run.",
        "height": 520,
        "width": 560,
        "color": 7
      },
      "name": "Section 2: Read manifest",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        520,
        80
      ]
    },
    {
      "parameters": {
        "content": "## 3. Query OSV.dev\nFree querybatch, then fetch each advisory for severity. Fail closed on HTTP errors.",
        "height": 560,
        "width": 720,
        "color": 7
      },
      "name": "Section 3: Query OSV.dev",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1120,
        80
      ]
    },
    {
      "parameters": {
        "content": "## 4. Triage and Slack\nOnly findings at or above minSeverity are posted. Clean runs stay silent.",
        "height": 560,
        "width": 720,
        "color": 7
      },
      "name": "Section 4: Triage and Slack",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1880,
        80
      ]
    },
    {
      "parameters": {
        "content": "## OSV needs no token\nDo not paste secrets into the HTTP nodes. Only GitHub and Slack use credentials.",
        "height": 200,
        "width": 340,
        "color": 3
      },
      "name": "Warn: OSV needs no token",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1120,
        660
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "name": "Every Monday 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.4,
      "position": [
        0,
        280
      ]
    },
    {
      "parameters": {},
      "name": "Run now",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        500
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "a0",
              "name": "githubOwner",
              "value": "YOUR_GITHUB_OWNER",
              "type": "string"
            },
            {
              "id": "a1",
              "name": "githubRepo",
              "value": "YOUR_REPO",
              "type": "string"
            },
            {
              "id": "a2",
              "name": "gitRef",
              "value": "main",
              "type": "string"
            },
            {
              "id": "a3",
              "name": "manifestPath",
              "value": "package.json",
              "type": "string"
            },
            {
              "id": "a4",
              "name": "minSeverity",
              "value": "HIGH",
              "type": "string"
            },
            {
              "id": "a5",
              "name": "maxPackages",
              "value": 60,
              "type": "number"
            },
            {
              "id": "a6",
              "name": "includeDevDependencies",
              "value": "false",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        380
      ]
    },
    {
      "parameters": {
        "resource": "file",
        "operation": "get",
        "owner": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.githubOwner }}"
        },
        "repository": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.githubRepo }}"
        },
        "filePath": "={{ $json.manifestPath }}",
        "asBinaryProperty": true,
        "binaryPropertyName": "data",
        "additionalParameters": {
          "reference": "={{ $json.gitRef }}"
        }
      },
      "name": "Read manifest from GitHub",
      "type": "n8n-nodes-base.github",
      "typeVersion": 1.1,
      "position": [
        560,
        380
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "errorMessage": "={{ 'Could not read ' + ($('Settings').first().json.manifestPath || 'manifest') + ' from ' + ($('Settings').first().json.githubOwner || '') + '/' + ($('Settings').first().json.githubRepo || '') + ' at ref ' + ($('Settings').first().json.gitRef || '') + '. Check the path, ref and GitHub credential.' }}"
      },
      "name": "Stop: Manifest missing",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        820,
        600
      ]
    },
    {
      "parameters": {
        "operation": "text",
        "binaryPropertyName": "data",
        "destinationKey": "data",
        "options": {
          "keepSource": "json"
        }
      },
      "name": "Extract manifest text",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        820,
        380
      ]
    },
    {
      "parameters": {
        "jsCode": "const cfg = $('Settings').first().json;\nconst text = String($json.data ?? $json.content ?? '');\nconst maxPackages = Math.min(Math.max(Number(cfg.maxPackages) || 60, 1), 200);\nconst minSeverity = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'].includes(String(cfg.minSeverity || '').toUpperCase())\n  ? String(cfg.minSeverity).toUpperCase() : 'HIGH';\nconst includeDev = String(cfg.includeDevDependencies || 'false').toLowerCase() === 'true';\nconst owner = String(cfg.githubOwner || '').trim();\nconst repo = String(cfg.githubRepo || '').trim();\nconst manifestPath = String(cfg.manifestPath || 'package.json').trim();\n\nfunction bareVersion(v) {\n  const s = String(v || '').trim();\n  const m = s.match(/(\\d+\\.\\d+\\.\\d+[0-9A-Za-z.+\\-]*)/);\n  return m ? m[1] : '';\n}\n\nconst packages = [];\nconst seen = new Set();\nfunction add(ecosystem, name, version) {\n  const ver = bareVersion(version);\n  if (!name || !ver) return;\n  const key = ecosystem + '|' + name + '|' + ver;\n  if (seen.has(key)) return;\n  seen.add(key);\n  if (packages.length >= maxPackages) return;\n  packages.push({ ecosystem, name, version: ver });\n}\n\nconst trimmed = text.trim();\nif (!trimmed) {\n  return [{ json: { valid: false, reason: 'empty_manifest', packages: [], package_count: 0, owner, repo, manifestPath, minSeverity } }];\n}\n\nif (manifestPath.endsWith('package-lock.json') || trimmed.startsWith('{') && trimmed.includes('\"lockfileVersion\"')) {\n  let lock;\n  try { lock = JSON.parse(trimmed); } catch (e) {\n    return [{ json: { valid: false, reason: 'invalid_json', packages: [], package_count: 0, owner, repo, manifestPath, minSeverity } }];\n  }\n  if (lock.packages && typeof lock.packages === 'object') {\n    for (const [path, meta] of Object.entries(lock.packages)) {\n      if (!path || path === '') continue; // root\n      if (!includeDev && meta.dev) continue;\n      const name = meta.name || path.replace(/^node_modules\\//, '').replace(/\\/node_modules\\//g, '/');\n      // package-lock paths like node_modules/lodash or node_modules/@scope/name\n      let pkgName = meta.name;\n      if (!pkgName) {\n        const parts = path.split('node_modules/').filter(Boolean);\n        pkgName = parts[parts.length - 1];\n      }\n      add('npm', pkgName, meta.version);\n    }\n  } else if (lock.dependencies && typeof lock.dependencies === 'object') {\n    const walk = (deps) => {\n      for (const [name, meta] of Object.entries(deps || {})) {\n        if (!includeDev && meta.dev) continue;\n        add('npm', name, meta.version);\n        if (meta.dependencies) walk(meta.dependencies);\n      }\n    };\n    walk(lock.dependencies);\n  }\n} else if (manifestPath.endsWith('requirements.txt') || (!trimmed.startsWith('{') && /==/.test(trimmed) && !trimmed.includes('\"name\"'))) {\n  for (const line of trimmed.split(/\\r?\\n/)) {\n    const t = line.trim();\n    if (!t || t.startsWith('#')) continue;\n    const m = t.match(/^([A-Za-z0-9_.\\-]+)\\s*==\\s*([0-9A-Za-z_.+\\-]+)/);\n    if (m) add('PyPI', m[1], m[2]);\n  }\n} else {\n  // package.json (or similar)\n  let pkg;\n  try { pkg = JSON.parse(trimmed); } catch (e) {\n    return [{ json: { valid: false, reason: 'invalid_json', packages: [], package_count: 0, owner, repo, manifestPath, minSeverity } }];\n  }\n  const sections = ['dependencies', 'optionalDependencies'];\n  if (includeDev) sections.push('devDependencies', 'peerDependencies');\n  for (const sec of sections) {\n    for (const [name, ver] of Object.entries(pkg[sec] || {})) add('npm', name, ver);\n  }\n}\n\nreturn [{ json: {\n  valid: packages.length > 0,\n  reason: packages.length ? 'ok' : 'no_packages',\n  packages,\n  package_count: packages.length,\n  owner,\n  repo,\n  manifestPath,\n  minSeverity,\n  queries: packages.map(p => ({ package: { ecosystem: p.ecosystem, name: p.name }, version: p.version })),\n} }];\n"
      },
      "name": "Parse packages",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1080,
        380
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "b667b88c-3ff3-5efb-a153-dd1084fc18ab",
              "leftValue": "={{ $json.valid }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Has packages?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1340,
        380
      ]
    },
    {
      "parameters": {
        "errorMessage": "={{ 'No packages to check in ' + ($json.manifestPath || 'manifest') + ' (' + ($json.reason || 'empty') + '). Set manifestPath to package-lock.json, package.json or requirements.txt with pinned versions.' }}"
      },
      "name": "Stop: No packages",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1600,
        600
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.osv.dev/v1/querybatch",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ queries: $json.queries }) }}",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "fullResponse": false,
              "neverError": false
            }
          }
        }
      },
      "name": "Query OSV querybatch",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        1600,
        300
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 4000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "errorMessage": "={{ 'OSV.dev querybatch failed: ' + ($json.error && $json.error.message ? $json.error.message : JSON.stringify($json.error || $json)).toString().slice(0, 400) }}"
      },
      "name": "Stop: OSV check failed",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1860,
        520
      ]
    },
    {
      "parameters": {
        "jsCode": "const parsed = $('Parse packages').first().json;\nconst batch = $input.first().json;\nconst results = Array.isArray(batch.results) ? batch.results : [];\nconst packages = parsed.packages || [];\nconst findings = []; // {id, ecosystem, name, version}\nconst seen = new Set();\n\nfor (let i = 0; i < results.length; i++) {\n  const pkg = packages[i] || {};\n  const vulns = (results[i] && results[i].vulns) || [];\n  for (const v of vulns) {\n    const id = v && v.id;\n    if (!id || seen.has(id + '|' + pkg.name + '|' + pkg.version)) continue;\n    seen.add(id + '|' + pkg.name + '|' + pkg.version);\n    findings.push({\n      id,\n      ecosystem: pkg.ecosystem || 'npm',\n      name: pkg.name || 'unknown',\n      version: pkg.version || '',\n      owner: parsed.owner,\n      repo: parsed.repo,\n      minSeverity: parsed.minSeverity,\n      package_count: parsed.package_count,\n    });\n  }\n}\n\n// Cap detail fetches so a huge lockfile cannot explode the run\nconst capped = findings.slice(0, 40);\nif (!capped.length) {\n  return [{ json: {\n    has_advisories: false,\n    alert: false,\n    findings: [],\n    owner: parsed.owner,\n    repo: parsed.repo,\n    package_count: parsed.package_count,\n    minSeverity: parsed.minSeverity,\n    text: `OSV check clean for ${parsed.owner}/${parsed.repo}: ${parsed.package_count} packages, no advisories.`,\n  } }];\n}\nreturn capped.map(f => ({ json: { ...f, has_advisories: true } }));\n"
      },
      "name": "List advisory ids",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1860,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "10ec2bd4-a368-5252-ae57-85bdaeeb2455",
              "leftValue": "={{ $json.has_advisories }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Any advisories?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        2120,
        300
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.osv.dev/v1/vulns/{{ $json.id }}",
        "options": {
          "timeout": 30000
        }
      },
      "name": "Fetch advisory details",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        2380,
        200
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "errorMessage": "={{ 'OSV.dev advisory fetch failed for ' + ($json.id || 'unknown') + ': check failed closed.' }}"
      },
      "name": "Stop: Advisory fetch failed",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        2640,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "const parsed = $('Parse packages').first().json;\nconst listed = $('List advisory ids').all().map(i => i.json).filter(j => j.has_advisories);\nconst byId = {};\nfor (const L of listed) {\n  byId[L.id] = byId[L.id] || [];\n  byId[L.id].push(L);\n}\n\nconst minSeverity = parsed.minSeverity || 'HIGH';\nconst owner = parsed.owner;\nconst repo = parsed.repo;\nconst package_count = parsed.package_count || 0;\nconst rank = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, MODERATE: 2, LOW: 1, UNKNOWN: 0 };\nconst minRank = rank[minSeverity] || 3;\n\nfunction severityOf(adv) {\n  const ds = String((adv.database_specific && adv.database_specific.severity) || '').toUpperCase();\n  if (ds === 'CRITICAL' || ds === 'HIGH' || ds === 'LOW') return ds;\n  if (ds === 'MEDIUM' || ds === 'MODERATE') return 'MEDIUM';\n  const cvss = adv.severity;\n  if (Array.isArray(cvss)) {\n    for (const s of cvss) {\n      const raw = String(s.score || '');\n      // numeric score\n      if (/^\\d+(\\.\\d+)?$/.test(raw)) {\n        const n = parseFloat(raw);\n        if (n >= 9) return 'CRITICAL';\n        if (n >= 7) return 'HIGH';\n        if (n >= 4) return 'MEDIUM';\n        if (n > 0) return 'LOW';\n      }\n      // vector — use database_specific already tried; leave UNKNOWN\n    }\n  }\n  return 'UNKNOWN';\n}\n\nfunction fixedVersion(adv) {\n  for (const a of adv.affected || []) {\n    for (const r of a.ranges || []) {\n      for (const ev of r.events || []) {\n        if (ev.fixed) return ev.fixed;\n      }\n    }\n  }\n  return '';\n}\n\nconst rows = [];\nfor (const adv of $input.all().map(i => i.json)) {\n  if (!adv || !adv.id) continue;\n  const sev = severityOf(adv);\n  const fixed = fixedVersion(adv);\n  const pkgs = byId[adv.id] || [{ name: 'unknown', version: '', ecosystem: 'npm' }];\n  for (const p of pkgs) {\n    rows.push({\n      id: adv.id,\n      severity: sev,\n      name: p.name,\n      version: p.version,\n      ecosystem: p.ecosystem,\n      summary: String(adv.summary || '').slice(0, 160),\n      fixed,\n    });\n  }\n}\n\nconst hitting = rows.filter(r => (rank[r.severity] || 0) >= minRank)\n  .sort((a, b) => (rank[b.severity] || 0) - (rank[a.severity] || 0));\nconst top = hitting.slice(0, 15);\nconst alert = top.length > 0;\nconst lines = top.map(r => `• ${r.severity} ${r.id} ${r.name}@${r.version}` + (r.fixed ? ` → fix ${r.fixed}` : ''));\nconst text = alert\n  ? `OSV findings for ${owner}/${repo} (≥ ${minSeverity}): ${top.length} shown (${hitting.length} total) across ${package_count} packages\\n` + lines.join('\\n')\n  : `OSV check for ${owner}/${repo}: ${package_count} packages, ${rows.length} advisory rows, none ≥ ${minSeverity}.`;\n\nreturn [{ json: {\n  alert, owner, repo, package_count, minSeverity,\n  finding_count: rows.length, alert_count: hitting.length, top, text,\n} }];\n"
      },
      "name": "Triage and build Slack text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2640,
        200
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "91d2bf82-5f08-5be5-a1b3-17a7ce0c8fba",
              "leftValue": "={{ $json.alert }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Above severity threshold?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        2900,
        200
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "messageType": "text",
        "text": "={{ $json.text }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "name": "Post alert to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.7,
      "position": [
        3160,
        100
      ]
    },
    {
      "parameters": {},
      "name": "All clear",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        3160,
        360
      ]
    }
  ],
  "connections": {
    "Every Monday 08:00": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run now": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Read manifest from GitHub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read manifest from GitHub": {
      "main": [
        [
          {
            "node": "Extract manifest text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop: Manifest missing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract manifest text": {
      "main": [
        [
          {
            "node": "Parse packages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse packages": {
      "main": [
        [
          {
            "node": "Has packages?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has packages?": {
      "main": [
        [
          {
            "node": "Query OSV querybatch",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop: No packages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query OSV querybatch": {
      "main": [
        [
          {
            "node": "List advisory ids",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop: OSV check failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List advisory ids": {
      "main": [
        [
          {
            "node": "Any advisories?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any advisories?": {
      "main": [
        [
          {
            "node": "Fetch advisory details",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "All clear",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch advisory details": {
      "main": [
        [
          {
            "node": "Triage and build Slack text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop: Advisory fetch failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Triage and build Slack text": {
      "main": [
        [
          {
            "node": "Above severity threshold?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Above severity threshold?": {
      "main": [
        [
          {
            "node": "Post alert to Slack",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "All clear",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "timezone": "UTC"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
