{
  "name": "Detect competitor Shopify sales, launches and price changes with Apify, Google Sheets and Slack",
  "nodes": [
    {
      "parameters": {
        "content": "## Know when a competitor's Shopify store starts a sale\n\n### How it works\nEvery morning this workflow reads the public catalog of the Shopify stores you list with the [Shopify Store Catalog & Pricing Extractor](https://apify.com/nimbryxo-studio/shopify-store-catalog-and-pricing-extractor) Actor on Apify and compares it with the copy saved in Google Sheets.\n\nSlack gets a few lines per store when the share of marked-down products jumps (a sale started) or falls back (it ended), when products launch or disappear, and when regular prices move more than your threshold. Quiet days send nothing.\n\nIt's for brands and resellers who time their own promotions against a handful of competitors. A product counts as marked down when its compare-at price is at least `minDiscountPct` above the price. Removals are only reported when the whole catalog was read, so raise `maxProductsPerStore` for big stores.\n\n### Setup\n1. In Apify Console > Settings > API & Integrations, copy your token. In n8n, create a **Header Auth** credential (name `Authorization`, value `Bearer <token>`) and select it in the three Apify nodes.\n2. Add a Google Sheets tab named `Catalog` with this header row: `key, store, handle, title, productType, price, compareAtPrice, discountPct, available, status, firstSeen, lastSeen, url`\n3. Connect Google Sheets and Slack.\n4. Fill in **Config**: store URLs, thresholds, sheet ID and channel.\n\n### Customization\nLower `saleJumpPoints` to catch smaller promotions. Each run is billed to your Apify account; `maxCostUsd` caps it.",
        "height": 836,
        "width": 560
      },
      "id": "e3c610c1-17af-40f4-a0bd-e22f18c2710a",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -660,
        -356
      ]
    },
    {
      "parameters": {
        "content": "## 1. Settings and last catalog\nChecks Config before anything costs money, then reads the catalog saved last time.",
        "height": 388,
        "width": 940,
        "color": 7
      },
      "id": "6cb60931-af2c-4352-aa4e-9ab66ef459a5",
      "name": "Section: settings",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -188
      ]
    },
    {
      "parameters": {
        "content": "## 2. Run the Actor and wait\nStarts the Apify run and checks its status every minute until it ends. A failed or stuck run goes to the alert below.",
        "height": 828,
        "width": 460,
        "color": 7
      },
      "id": "00a0745a-b386-4a02-a471-e7047c148277",
      "name": "Section: run",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        920,
        -188
      ]
    },
    {
      "parameters": {
        "content": "## 3. Compare catalogs\nPer store: did the share of marked-down products jump (sale started) or fall (sale over), what launched, what disappeared, which prices moved.",
        "height": 388,
        "width": 460,
        "color": 7
      },
      "id": "c1e113ef-d217-4fae-aa53-f6e1bf55eee8",
      "name": "Section: compare",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1400,
        -188
      ]
    },
    {
      "parameters": {
        "content": "## 4. Save and tell Slack\nUpdates one row per product (matched on `key`), keeps vanished ones as \"removed\", and posts only when something changed.",
        "height": 828,
        "width": 460,
        "color": 7
      },
      "id": "932e5742-4e06-4ad3-a7e7-1d491ae42da7",
      "name": "Section: deliver",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1880,
        -188
      ]
    },
    {
      "parameters": {
        "content": "## If the run fails\nPosts the run status and a console link, then stops with an error so the execution shows as failed.",
        "height": 366,
        "width": 460,
        "color": 7
      },
      "id": "9787a630-b757-454d-a840-8b973523d213",
      "name": "Section: failure",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1400,
        274
      ]
    },
    {
      "parameters": {
        "content": "## Paid run\nStarts a run on your Apify account. `maxCostUsd` in Config caps what one run can spend.",
        "height": 148,
        "width": 300,
        "color": 3
      },
      "id": "752049c5-02c6-4449-a7b8-3589d1493d92",
      "name": "Warning: paid run",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        860,
        -356
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            }
          ]
        }
      },
      "id": "52bb5d0f-4280-4619-a3e4-be6d18dfeacc",
      "name": "Every day at 09:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"storeUrls\": [\n    \"https://deathwishcoffee.com\"\n  ],\n  \"maxProductsPerStore\": 250,\n  \"minDiscountPct\": 10,\n  \"saleJumpPoints\": 15,\n  \"priceMovePct\": 10,\n  \"googleSheetId\": \"YOUR_GOOGLE_SHEET_ID\",\n  \"sheetTab\": \"Catalog\",\n  \"slackChannel\": \"#competitor-watch\",\n  \"maxCostUsd\": 1,\n  \"maxWaitMinutes\": 20\n}\n",
        "options": {}
      },
      "id": "77dba60d-aa38-483b-a40b-1dc8d276bdbc",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        240,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Checks Config before any paid Apify run and builds the Actor input.\nconst cfg = $('Config').first().json;\nconst problems = [];\nconst stores = [...new Set((cfg.storeUrls || []).map((u) => String(u).trim().replace(/\\/+$/, '')).filter(Boolean)\n  .map((u) => (/^https?:\\/\\//i.test(u) ? u : `https://${u}`).toLowerCase()))];\nif (!stores.length) problems.push('add at least one store to \"storeUrls\", for example \"https://deathwishcoffee.com\"');\nconst maxProducts = Number(cfg.maxProductsPerStore);\nif (!Number.isInteger(maxProducts) || maxProducts < 1 || maxProducts > 5000) problems.push('\"maxProductsPerStore\" must be a whole number from 1 to 5000');\nif (!cfg.googleSheetId || /YOUR_/i.test(String(cfg.googleSheetId))) problems.push('set \"googleSheetId\" to the ID in your sheet URL');\nif (!cfg.slackChannel) problems.push('set \"slackChannel\"');\nif (problems.length) throw new Error(`Config: ${problems.join('; ')}`);\n\nconst actorInput = {\n  mode: 'catalog',\n  storeUrls: stores,\n  maxItems: maxProducts * stores.length,\n  currency: 'store',\n  available: 'any',\n  includeDetails: false,\n  flattenVariants: false,\n};\nreturn [{ json: { actorInput, stores, maxProducts } }];\n"
      },
      "id": "8654bd59-1513-4b5a-a50e-d95a4b0d8be6",
      "name": "Check settings",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        0
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Config').first().json.googleSheetId }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Config').first().json.sheetTab }}"
        },
        "options": {}
      },
      "id": "8c5a8051-5f5e-4998-a396-dfd6c07ce30f",
      "name": "Read previous run from Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        720,
        0
      ],
      "executeOnce": true,
      "alwaysOutputData": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 3000
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/nimbryxo-studio~shopify-store-catalog-and-pricing-extractor/runs",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "waitForFinish",
              "value": "60"
            },
            {
              "name": "maxTotalChargeUsd",
              "value": "={{ $('Config').first().json.maxCostUsd }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($('Check settings').first().json.actorInput) }}",
        "options": {
          "timeout": 90000
        }
      },
      "id": "8178107e-0e0c-43ae-a322-4d1423631dfb",
      "name": "Start Apify run",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        960,
        0
      ],
      "executeOnce": true
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "succeeded",
                    "leftValue": "={{ $json.data.status }}",
                    "rightValue": "SUCCEEDED",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Succeeded"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "running",
                    "leftValue": "={{ ['READY', 'RUNNING'].includes($json.data.status) && $runIndex < $('Config').first().json.maxWaitMinutes }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Still running"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "Failed or stuck"
        }
      },
      "id": "d8bf0d51-1aaa-4b7a-a498-42fbd1ff75ae",
      "name": "Route by run status",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1200,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ 'https://api.apify.com/v2/actor-runs/' + $json.data.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "waitForFinish",
              "value": "60"
            }
          ]
        },
        "options": {
          "timeout": 90000
        }
      },
      "id": "7a041021-139e-486e-a684-5b91e0684979",
      "name": "Check run status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1200,
        440
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "url": "={{ 'https://api.apify.com/v2/datasets/' + $json.data.defaultDatasetId + '/items' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "clean",
              "value": "true"
            },
            {
              "name": "format",
              "value": "json"
            }
          ]
        },
        "options": {
          "timeout": 120000
        }
      },
      "id": "e8a99b99-8c2b-405c-a669-12b78506a9e3",
      "name": "Download results",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1440,
        0
      ],
      "alwaysOutputData": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Reads each competitor's catalog the way a merchandiser would: did a sale start (a jump in the share of products\n// marked down), what launched, what disappeared, which prices moved. Returns ONE item: { rows } for the sheet and\n// { report, message } for Slack. Products that vanish are kept in the sheet with status \"removed\".\nconst cfg = $('Config').first().json;\nconst settings = $('Check settings').first().json;\nconst today = new Date().toISOString().slice(0, 10);\nconst num = (v) => {\n  if (v === null || v === undefined || v === '') return null;\n  const n = Number(String(v).replace(/[^0-9.-]/g, ''));\n  return Number.isFinite(n) ? n : null;\n};\nconst host = (u) => String(u || '').toLowerCase().replace(/^https?:\\/\\//, '').replace(/^www\\./, '').replace(/\\/.*$/, '');\nconst minDiscount = num(cfg.minDiscountPct) ?? 10;\nconst jumpPts = num(cfg.saleJumpPoints) ?? 15;\nconst movePct = num(cfg.priceMovePct) ?? 10;\nconst listMax = 8;\n\nconst saved = new Map();\nfor (const item of $('Read previous run from Google Sheets').all()) {\n  const r = item.json || {};\n  if (r.key) saved.set(String(r.key), r);\n}\nconst products = $input.all().map((i) => i.json || {}).filter((p) => (p.handle || p.url) && norm(p.recordType) !== 'error');\nfunction norm(s) { return String(s ?? '').trim().toLowerCase(); }\n// The catalog was read to the end when the run stopped before its row cap.\nconst complete = products.length < settings.actorInput.maxItems;\n\nconst discountOf = (p) => {\n  const given = num(p.discountPercent);\n  if (given !== null) return given;\n  const price = num(p.price);\n  const was = num(p.compareAtPrice);\n  return price !== null && was !== null && was > price ? (was - price) / was * 100 : 0;\n};\n\nconst rows = [];\nconst blocks = [];\nfor (const store of settings.stores) {\n  const h = host(store);\n  const mine = products.filter((p) => host(p.storeUrl || (p.store && p.store.url) || p.url) === h);\n  const before = [...saved.values()].filter((r) => r.store === h && r.status !== 'removed');\n  const beforeByKey = new Map(before.map((r) => [String(r.key), r]));\n  const seen = new Set();\n  const launched = [];\n  const moved = [];\n  let onSaleNow = 0;\n  const discounts = [];\n  for (const p of mine) {\n    const key = `${h}/${p.handle || String(p.url).split('/products/')[1] || p.id}`;\n    seen.add(key);\n    const price = num(p.price);\n    const d = discountOf(p);\n    if (d >= minDiscount) { onSaleNow += 1; discounts.push(d); }\n    const prev = saved.get(key);\n    if (!prev || prev.status === 'removed') launched.push(p);\n    else {\n      const was = num(prev.price);\n      if (was && price !== null && Math.abs(price - was) / was * 100 >= movePct) moved.push(`${p.title}: ${was} -> ${price}`);\n    }\n    rows.push({\n      key, store: h, handle: p.handle || '', title: p.title || '', productType: p.productType || '',\n      price: price ?? '', compareAtPrice: num(p.compareAtPrice) ?? '', discountPct: Math.round(d * 10) / 10,\n      available: p.available === false ? 'no' : 'yes', status: 'listed',\n      firstSeen: (prev && prev.firstSeen) || today, lastSeen: today, url: p.url || '',\n    });\n  }\n  const removed = complete && mine.length ? before.filter((r) => !seen.has(String(r.key))) : [];\n  for (const r of removed) rows.push({ ...r, status: 'removed' });\n\n  if (!before.length) {\n    blocks.push(mine.length\n      ? `*${h}*: first look, ${mine.length} products saved as a baseline; ${onSaleNow ? `${onSaleNow} of them (${Math.round(onSaleNow / mine.length * 100)}%) marked down by ${minDiscount}% or more right now` : 'none marked down right now'}.`\n      : `*${h}*: no products came back. Check that it is a Shopify store with a public catalog.`);\n    continue;\n  }\n  if (!mine.length) { blocks.push(`*${h}*: no products came back today (it had ${before.length}). The store may be down or no longer on Shopify.`); continue; }\n  const shareNow = onSaleNow / mine.length * 100;\n  const shareBefore = before.filter((r) => num(r.discountPct) >= minDiscount).length / before.length * 100;\n  const lines = [];\n  if (shareNow - shareBefore >= jumpPts) {\n    const median = discounts.sort((a, b) => a - b)[Math.floor(discounts.length / 2)] || 0;\n    lines.push(`:rotating_light: *Sale started*: ${Math.round(shareNow)}% of products marked down (was ${Math.round(shareBefore)}%), median discount ${Math.round(median)}%.`);\n  } else if (shareBefore - shareNow >= jumpPts) {\n    lines.push(`:information_source: Sale over: ${Math.round(shareNow)}% of products marked down (was ${Math.round(shareBefore)}%).`);\n  }\n  if (launched.length) lines.push(`:new: ${launched.length} new product${launched.length === 1 ? '' : 's'}: ${launched.slice(0, listMax).map((p) => `<${p.url}|${p.title}>`).join(', ')}${launched.length > listMax ? ', ...' : ''}`);\n  if (removed.length) lines.push(`:wastebasket: ${removed.length} product${removed.length === 1 ? '' : 's'} gone: ${removed.slice(0, listMax).map((r) => r.title).join(', ')}${removed.length > listMax ? ', ...' : ''}`);\n  if (moved.length) lines.push(`:arrow_right: ${moved.length} price change${moved.length === 1 ? '' : 's'} of ${movePct}%+: ${moved.slice(0, listMax).join('; ')}${moved.length > listMax ? '; ...' : ''}`);\n  if (lines.length) blocks.push([`*${h}* (${mine.length} products)`, ...lines].join('\\n'));\n}\n\nconst firstRun = saved.size === 0;\nconst report = blocks.length > 0;\nconst message = report ? [`*Competitor catalog watch*${firstRun ? ' is set up' : ''}`, ...blocks].join('\\n\\n') : '';\nreturn [{ json: { report, message, rows, checked: products.length, complete } }];\n"
      },
      "id": "20cab021-9458-4820-a556-4232407a55c0",
      "name": "Compare catalogs with the last run",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1680,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// One item per sheet row (none on a day without rows, so the sheet step is skipped).\nreturn ($json.rows || []).map((row) => ({ json: row }));"
      },
      "id": "7771de38-8310-4360-a800-9160c277a3c7",
      "name": "Rows for the sheet",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1920,
        0
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Config').first().json.googleSheetId }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Config').first().json.sheetTab }}"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "key"
          ],
          "schema": []
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "e7d54e23-aa57-4f7b-a4b5-5fdde76585f4",
      "name": "Save to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2160,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 3000
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "report",
              "leftValue": "={{ $json.report }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a8f0ea98-21b4-4e3a-aa39-f741d1795a4b",
      "name": "Anything to report?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1920,
        440
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Config').first().json.slackChannel }}"
        },
        "text": "={{ $json.message }}",
        "otherOptions": {
          "mrkdwn": true
        }
      },
      "id": "13084c71-1c3b-48d7-a7b1-7ae802f75dcd",
      "name": "Post the report to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        2160,
        440
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Config').first().json.slackChannel }}"
        },
        "text": "={{ ':warning: *Detect competitor Shopify sales, launches and price changes*: the Apify run ended as *' + $json.data.status + '*' + (['READY','RUNNING'].includes($json.data.status) ? ' (still running after ' + $('Config').first().json.maxWaitMinutes + ' minutes)' : '') + '. Run: https://console.apify.com/view/runs/' + $json.data.id }}",
        "otherOptions": {
          "mrkdwn": true
        }
      },
      "id": "4feac85a-1a65-40bb-a91e-c10a5e91567e",
      "name": "Alert: run failed",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1440,
        440
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 3000
    },
    {
      "parameters": {
        "errorMessage": "={{ 'Apify run ' + $('Route by run status').item.json.data.status + ': https://console.apify.com/view/runs/' + $('Route by run status').item.json.data.id }}"
      },
      "id": "ab3121f5-08ff-4251-a13b-679e29802f12",
      "name": "Stop: run failed",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1680,
        440
      ]
    }
  ],
  "connections": {
    "Every day at 09:00": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Check settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check settings": {
      "main": [
        [
          {
            "node": "Read previous run from Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read previous run from Google Sheets": {
      "main": [
        [
          {
            "node": "Start Apify run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start Apify run": {
      "main": [
        [
          {
            "node": "Route by run status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by run status": {
      "main": [
        [
          {
            "node": "Download results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check run status",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Alert: run failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check run status": {
      "main": [
        [
          {
            "node": "Route by run status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download results": {
      "main": [
        [
          {
            "node": "Compare catalogs with the last run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare catalogs with the last run": {
      "main": [
        [
          {
            "node": "Rows for the sheet",
            "type": "main",
            "index": 0
          },
          {
            "node": "Anything to report?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rows for the sheet": {
      "main": [
        [
          {
            "node": "Save to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anything to report?": {
      "main": [
        [
          {
            "node": "Post the report to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert: run failed": {
      "main": [
        [
          {
            "node": "Stop: run failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
