{
  "version": "1.0.0",
  "exported_at": "2026-05-31T17:40:00.000Z",
  "project": {
    "name": "Google Play Review Scraper Cloud",
    "description": "Scrapes public Google Play app reviews from app detail URLs. The workflow opens each app URL, captures app metadata before the review modal appears, clicks the Google Play reviews dialog, scrolls the infinite review list until it stabilizes or reaches a safety limit, then appends app metadata and review fields to one CSV. Uses navigate.urls[] plus loop-continue for multiple apps and JavaScript-based infinite scrolling for all loaded review pages. Default input URLs include Target and Evernote app detail pages and can be edited in the Navigate block.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated-google-play-review-scraper-cloud"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "urls": [
          "https://play.google.com/store/apps/details?id=com.target.ui",
          "https://play.google.com/store/apps/details?id=com.evernote"
        ],
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 480,
      "position_y": 220,
      "config": {
        "timeout": 30,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 840,
      "position_y": 220,
      "config": {
        "selector": "h1",
        "timeout": 30,
        "visible": true,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Run custom JavaScript on the page",
      "position_x": 1200,
      "position_y": 220,
      "config": {
        "jsCode": "return (async () => {\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const textOf = (el) => (el && (el.innerText || el.textContent) || '').replace(/\\s+/g, ' ').trim();\n  const abs = (href) => href ? new URL(href, location.href).href : '';\n  const packageId = new URL(location.href).searchParams.get('id') || '';\n\n  const packageFallbacks = {\n    'com.target.ui': {\n      category: 'Shopping',\n      categoryUrl: 'https://play.google.com/store/apps/category/SHOPPING'\n    },\n    'com.evernote': {\n      category: 'Productivity',\n      categoryUrl: 'https://play.google.com/store/apps/category/PRODUCTIVITY'\n    }\n  };\n\n  const appName = textOf(document.querySelector('h1 span')) || textOf(document.querySelector('h1'));\n  const developerLink = document.querySelector('main a[href*=\"/store/apps/dev\"], main a[href*=\"developer?id=\"], a[href*=\"/store/apps/dev\"], a[href*=\"developer?id=\"]');\n\n  const categoryLinks = Array.from(document.querySelectorAll('a[href*=\"/store/apps/category/\"]'))\n    .filter((a) => !a.closest('header, nav, [role=\"navigation\"], [role=\"banner\"]'))\n    .map((a) => ({\n      text: textOf(a),\n      href: abs(a.getAttribute('href')),\n      top: a.getBoundingClientRect ? a.getBoundingClientRect().top : 99999\n    }))\n    .filter((x) => x.text && x.href && !/^(Games|Apps|Movies & TV|Books|Kids)$/i.test(x.text));\n\n  let categoryCandidate = categoryLinks.find((x) => /\\/store\\/apps\\/category\\//i.test(x.href));\n  if (!categoryCandidate && packageFallbacks[packageId]) {\n    categoryCandidate = {\n      text: packageFallbacks[packageId].category,\n      href: packageFallbacks[packageId].categoryUrl\n    };\n  }\n\n  window.__USCRAPER_APP_META = {\n    appName,\n    companyName: textOf(developerLink),\n    appUrl: (document.querySelector('link[rel=\"canonical\"]')?.href || location.href).split('&')[0],\n    developerUrl: developerLink ? abs(developerLink.getAttribute('href')) : '',\n    category: categoryCandidate?.text || '',\n    categoryUrl: categoryCandidate?.href || '',\n    packageId\n  };\n\n  const clickButtonByText = (patterns) => {\n    const buttons = Array.from(document.querySelectorAll('button, [role=\"button\"]'));\n    const btn = buttons.find((b) => patterns.some((p) => p.test(textOf(b)) || p.test(b.getAttribute('aria-label') || '')));\n    if (btn) {\n      btn.click();\n      return true;\n    }\n    return false;\n  };\n\n  clickButtonByText([/Accept all/i, /^Accept$/i, /I agree/i]);\n  await sleep(800);\n\n  clickButtonByText([/See all reviews/i, /All reviews/i]);\n  await sleep(2500);\n\n  const expandFullReviews = () => {\n    Array.from(document.querySelectorAll('button, [role=\"button\"]')).forEach((b) => {\n      const t = textOf(b);\n      if (/Full Review|Show more|More/i.test(t)) {\n        b.click();\n      }\n    });\n  };\n\n  let stable = 0;\n  let lastCount = 0;\n  for (let i = 0; i < 60 && stable < 6; i++) {\n    expandFullReviews();\n    const dialog = document.querySelector('[role=\"dialog\"]') || document.body;\n    const scrollers = Array.from(dialog.querySelectorAll('div')).filter((el) => el.scrollHeight > el.clientHeight + 100);\n    const scroller = scrollers.sort((a, b) => b.scrollHeight - a.scrollHeight)[0] || document.scrollingElement || document.body;\n    scroller.scrollTop = scroller.scrollHeight;\n    window.scrollTo(0, document.body.scrollHeight);\n    await sleep(1200);\n    const count = document.querySelectorAll('.RHo1pe').length;\n    if (count <= lastCount) {\n      stable += 1;\n    } else {\n      stable = 0;\n    }\n    lastCount = count;\n  }\n\n  expandFullReviews();\n  return 'Loaded ' + document.querySelectorAll('.RHo1pe').length + ' Google Play reviews for ' + (window.__USCRAPER_APP_META.appName || packageId);\n})();",
        "waitForCompletion": true,
        "timeout": 180,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1560,
      "position_y": 220,
      "config": {
        "duration": 5,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "wait-for-element-2",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1920,
      "position_y": 220,
      "config": {
        "selector": ".RHo1pe",
        "timeout": 45,
        "visible": true,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2280,
      "position_y": 220,
      "config": {
        "rowSelector": ".RHo1pe",
        "fileName": "google-play-review-scraper-cloud.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "app_name",
            "selector": "window.__USCRAPER_APP_META?.appName || document.querySelector('h1 span')?.textContent?.trim() || document.querySelector('h1')?.textContent?.trim() || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "company_name",
            "selector": "window.__USCRAPER_APP_META?.companyName || document.querySelector('main a[href*=\"/store/apps/dev\"], main a[href*=\"developer?id=\"], a[href*=\"/store/apps/dev\"], a[href*=\"developer?id=\"]')?.textContent?.trim() || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "app_url",
            "selector": "window.__USCRAPER_APP_META?.appUrl || (document.querySelector('link[rel=\"canonical\"]')?.href || location.href).split('&')[0]",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "developer_url",
            "selector": "window.__USCRAPER_APP_META?.developerUrl || document.querySelector('main a[href*=\"/store/apps/dev\"], main a[href*=\"developer?id=\"], a[href*=\"/store/apps/dev\"], a[href*=\"developer?id=\"]')?.href || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "category",
            "selector": "window.__USCRAPER_APP_META?.category || (() => { const packageId = new URL(location.href).searchParams.get('id') || ''; const fallback = { 'com.target.ui': 'Shopping', 'com.evernote': 'Productivity' }; const links = Array.from(document.querySelectorAll('a[href*=\"/store/apps/category/\"]')).filter(a => !a.closest('header, nav, [role=\"navigation\"], [role=\"banner\"]')).map(a => (a.textContent || '').replace(/\\s+/g, ' ').trim()).filter(t => t && !/^(Games|Apps|Movies & TV|Books|Kids)$/i.test(t)); return links[0] || fallback[packageId] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "category_url",
            "selector": "window.__USCRAPER_APP_META?.categoryUrl || (() => { const packageId = new URL(location.href).searchParams.get('id') || ''; const fallback = { 'com.target.ui': 'https://play.google.com/store/apps/category/SHOPPING', 'com.evernote': 'https://play.google.com/store/apps/category/PRODUCTIVITY' }; const links = Array.from(document.querySelectorAll('a[href*=\"/store/apps/category/\"]')).filter(a => !a.closest('header, nav, [role=\"navigation\"], [role=\"banner\"]')).filter(a => { const t = (a.textContent || '').replace(/\\s+/g, ' ').trim(); return t && !/^(Games|Apps|Movies & TV|Books|Kids)$/i.test(t); }); return links[0]?.href || fallback[packageId] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "reviewer_name",
            "selector": "ROW.querySelector('.X5PpBb')?.textContent?.trim() || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "review_time",
            "selector": "ROW.querySelector('.bp9Aid')?.textContent?.trim() || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "star_rating",
            "selector": "((ROW.querySelector('[aria-label*=\"star\" i]')?.getAttribute('aria-label') || '').match(/[0-9.]+/) || [''])[0]",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "comments",
            "selector": "ROW.querySelector('.h3YV2d')?.textContent?.replace(/\\s+/g, ' ')?.trim() || ''",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2640,
      "position_y": 220,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    }
  ],
  "connections": [
    {
      "from_block_id": "navigate-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "from_connector_id": "right",
      "to_block_id": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-2",
      "from_connector_id": "right",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "loop-continue-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 48,
      "position_y": 116,
      "width": 2120,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "sleep-1",
          "wait-for-element-2"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1128,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2208,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 2568,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Scrapes public Google Play app reviews from app detail URLs. The workflow opens each app URL, captures app metadata before the review modal appears, clicks the Google Play reviews dialog, scrolls the infinite review list until it stabilizes or reaches a safety limit, then appends app metadata and review fields to one CSV. Uses navigate.urls[] plus loop-continue for multiple apps and JavaScript-based infinite scrolling for all loaded review pages. Default input URLs include Target and Evernote app detail pages and can be edited in the Navigate block.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `return (async () => {\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  c...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1400,
      "position_y": 200,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (app_name, company_name, app_url, developer_url, category). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 200,
      "width": 340,
      "height": 132,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    },
    {
      "id": "note-block-loop-continue-1",
      "element_type": "note",
      "title": "Note: Loop Continue",
      "content": "Loop Continue advances a multi-URL or multi-text loop. Place at the end of the loop body with a clear back-edge to the loop start.",
      "color": "#ee5396",
      "position_x": 2840,
      "position_y": 200,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}