{
  "name": "Shopify changes feed to Google Sheets",
  "nodes": [
    {
      "parameters": {},
      "id": "node-1",
      "name": "Manual run",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        300
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "run-id",
              "name": "runId",
              "value": "REPLACE_WITH_COMPLETED_APIFY_RUN_ID",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "node-2",
      "name": "Configure run",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const runId = $input.first().json.runId;\nif (typeof runId !== 'string' || !/^[A-Za-z0-9]{17}$/.test(runId)) throw new Error('Enter a completed Apify run ID in Configure run.');\nreturn [{json:{runId}}];"
      },
      "id": "node-3",
      "name": "Validate run ID",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ]
    },
    {
      "parameters": {
        "url": "=https://api.apify.com/v2/actor-runs/{{ $json.runId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "node-4",
      "name": "Read run",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const r = $input.first().json.data;\nif (!r || r.actId !== 'MNg7jZPBynPqwJ5mB' || r.status !== 'SUCCEEDED') throw new Error('Expected a SUCCEEDED run of shopify-price-stock-monitor; failed or unfinished runs are not a quiet check.');\nif (!/^[A-Za-z0-9]{17}$/.test(r.defaultDatasetId || '')) throw new Error('Missing dataset ID.');\nreturn [{json:{runId:r.id,datasetId:r.defaultDatasetId}}];"
      },
      "id": "node-5",
      "name": "Require successful run",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        300
      ]
    },
    {
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $json.datasetId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "node-6",
      "name": "Read dataset metadata",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const d = $input.first().json.data;\nconst r = $('Require successful run').first().json;\nif (!d || d.id !== r.datasetId || !Number.isInteger(d.itemCount) || d.itemCount < 1) throw new Error('Missing or empty dataset: inspect the run; do not record it as unchanged.');\nif (d.itemCount > 10000) throw new Error('Dataset exceeds this template limit of 10,000 rows. Add pagination before processing this run. No partial feed was written.');\nreturn [{json:{...r,itemCount:d.itemCount}}];"
      },
      "id": "node-7",
      "name": "Require complete bounded dataset",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        300
      ]
    },
    {
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $json.datasetId }}/items",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "format",
              "value": "json"
            },
            {
              "name": "clean",
              "value": "false"
            },
            {
              "name": "limit",
              "value": "10000"
            }
          ]
        }
      },
      "id": "node-8",
      "name": "Read all rows",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1540,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Require complete bounded dataset').first().json;\nconst rows = $input.all().flatMap(item => Array.isArray(item.json) ? item.json : [item.json]);\nif (rows.length !== meta.itemCount) throw new Error('Incomplete dataset read; no partial feed was written.');\nif (!rows.some(r => r.record_type === 'store_check')) throw new Error('Missing status rows; inspect the Actor output contract.');\nconst seen = new Set();\nconst out = [];\nfor (const r of rows) {\n  const failure = r.record_type === 'store_check' && !['SUCCESS','NO_RESULTS'].includes(r.status);\n  const event = r.record_type === 'change_event' && (typeof r.event_type === 'string' && r.event_type.length > 0) && (!r.status || r.status === 'SUCCESS');\n  if (!failure && !event) continue;\n  if (!r.source_id || !r.scraped_at) throw new Error('Missing stable identity or observation time.');\n  const kind = failure ? 'SOURCE_STATUS' : r.event_type;\n  const key = JSON.stringify([meta.runId,r.source_id,r.external_id || '',r.scraped_at,kind,r.fingerprint || '']);\n  if (seen.has(key)) continue;\n  seen.add(key);\n  out.push({json:{event_key:key,run_id:meta.runId,observed_at:r.scraped_at,row_kind:failure ? 'source_status' : 'change',\n    external_id:r.external_id || '',source_id:r.source_id,change_type:kind,status:r.status || 'SUCCESS',\n    source_url:r.source_url || '',details_json:JSON.stringify(r)}});\n}\nreturn out;\n"
      },
      "id": "node-9",
      "name": "Changes and source statuses",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        300
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Changes",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "event_key"
          ],
          "schema": [
            {
              "id": "event_key",
              "displayName": "event_key",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "run_id",
              "displayName": "run_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "observed_at",
              "displayName": "observed_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "row_kind",
              "displayName": "row_kind",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "external_id",
              "displayName": "external_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "source_id",
              "displayName": "source_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "change_type",
              "displayName": "change_type",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "source_url",
              "displayName": "source_url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "details_json",
              "displayName": "details_json",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW",
          "handlingExtraData": "error"
        }
      },
      "id": "node-10",
      "name": "Upsert Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1980,
        300
      ]
    },
    {
      "parameters": {
        "content": "## Shopify changes feed to Google Sheets\n1. Import this inactive workflow.\n2. Configure run: paste a completed run ID from your own shopify-price-stock-monitor.\n3. Select an HTTP Header Auth credential on all three HTTP Request nodes: Authorization / Bearer YOUR_APIFY_TOKEN. Store the token only in credentials.\n4. Select your Google Sheets credential and spreadsheet; create a Changes sheet with the documented headers.\n5. Execute manually. Replaying the same run upserts by event_key.\n\nNo Actor is started or scheduled. Failed/unfinished runs fail loudly. Source failures inside successful batches become source_status rows. Baselines and quiet successful checks produce no change rows. The 10,000-row guard prevents silent truncation. API/Sheets errors stop execution. Serialize imports to the same sheet: Google Sheets is not a transactional queue.",
        "height": 510,
        "width": 650
      },
      "id": "node-11",
      "name": "Setup and limits",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2200,
        300
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "Manual run": {
      "main": [
        [
          {
            "node": "Configure run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure run": {
      "main": [
        [
          {
            "node": "Validate run ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate run ID": {
      "main": [
        [
          {
            "node": "Read run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read run": {
      "main": [
        [
          {
            "node": "Require successful run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Require successful run": {
      "main": [
        [
          {
            "node": "Read dataset metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read dataset metadata": {
      "main": [
        [
          {
            "node": "Require complete bounded dataset",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Require complete bounded dataset": {
      "main": [
        [
          {
            "node": "Read all rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read all rows": {
      "main": [
        [
          {
            "node": "Changes and source statuses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Changes and source statuses": {
      "main": [
        [
          {
            "node": "Upsert Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}
