AutomationFlowsWeb Scraping › Monitor Tiktok Creator Growth with Apify, Google Sheets, and Telegram

Monitor Tiktok Creator Growth with Apify, Google Sheets, and Telegram

ByHanna Nosova @fetch-cat on n8n.io

This workflow runs manually or every Monday to monitor a TikTok creator watchlist by scraping profile metrics with Apify, logging snapshots in Google Sheets, and sending baseline or change alerts to Telegram. Runs on a manual trigger or on a weekly schedule every Monday at…

Event trigger★★★★☆ complexity25 nodesGoogle SheetsHTTP RequestTelegram
Web Scraping Trigger: Event Nodes: 25 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17718 — we link there as the canonical source.

This workflow follows the Google Sheets → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "BlVLCpgkhKefx8NN",
  "name": "Monitor TikTok Creator Growth with Apify, Google Sheets and Telegram",
  "tags": [],
  "nodes": [
    {
      "id": "61000000-0000-0000-4000-8000-000000000001",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -2080,
        160
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000002",
      "name": "Weekly Monday Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2080,
        -48
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000003",
      "name": "1. Read Creator Watchlist",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -1808,
        80
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "933636260",
          "cachedResultName": "Creators"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1YV806viFdmh0J6m2OuzZXhgDvrrWL8-GqU-DKh5ygKE",
          "cachedResultName": "FetchCat n8n QA - TikTok Profiles"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7,
      "alwaysOutputData": true
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000004",
      "name": "Validate Watchlist and Build Actor Input",
      "type": "n8n-nodes-base.code",
      "position": [
        -1568,
        80
      ],
      "parameters": {
        "jsCode": "const rows = $input.all().map((item) => item.json || {});\nconst isActive = (value) => !['false', 'no', '0', 'inactive', 'paused'].includes(String(value ?? '').trim().toLowerCase());\nconst numberOr = (value, fallback) => value === '' || value === null || value === undefined || !Number.isFinite(Number(value)) ? fallback : Number(value);\nconst configs = [];\nconst seen = new Set();\nfor (const row of rows) {\n  const source = String(row['TikTok handle or URL'] || row['TikTok handle'] || row.Handle || row.handle || '').trim();\n  if (!source || !isActive(row.Active)) continue;\n  const urlMatch = source.match(/(?:https?:\\/\\/)?(?:www\\.)?tiktok\\.com\\/@([A-Za-z0-9._-]+)/i);\n  const handle = (urlMatch?.[1] || source.replace(/^@/, '')).trim().toLowerCase();\n  if (!/^[a-z0-9._-]{2,64}$/.test(handle)) throw new Error('Invalid TikTok creator in Creators sheet: ' + source);\n  if (seen.has(handle)) continue;\n  seen.add(handle);\n  const minimumFollowers = Math.max(0, numberOr(row['Minimum followers'], 0));\n  const maximumFollowers = Math.max(minimumFollowers, numberOr(row['Maximum followers'], 1+1234567890));\n  configs.push({\n    handle,\n    label: String(row.Label || row.Niche || '').trim(),\n    minimumFollowers,\n    maximumFollowers,\n    alertGrowthPercent: Math.max(0, numberOr(row['Alert growth %'], 5)),\n    alertFollowerGain: Math.max(0, numberOr(row['Alert follower gain'], 1000))\n  });\n}\nif (configs.length === 0) throw new Error('Add at least one active creator to the Creators sheet.');\nif (configs.length > 500) throw new Error('The monitor supports at most 500 active creators per run.');\nreturn [{ json: {\n  configs,\n  actorInput: { handles: configs.map((entry) => entry.handle), profileUrls: [], maxProfiles: configs.length, includeRecentVideos: false, maxVideosPerProfile: 0, maxConcurrency: 3, runTimeSecs: 270, proxyConfiguration: { useApifyProxy: true } }\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000005",
      "name": "2. Run FetchCat TikTok Profile Scraper",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1328,
        80
      ],
      "parameters": {
        "url": "https://api.apify.com/v2/acts/B5qu4d1iteAp2TaF1/runs",
        "method": "POST",
        "options": {
          "timeout": 310000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={{ $json.actorInput }}",
        "sendBody": true,
        "sendQuery": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "waitForFinish",
              "value": "300"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept-Encoding",
              "value": "identity"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000006",
      "name": "Download Current TikTok Profiles",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1088,
        80
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $json.data.defaultDatasetId }}/items",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "clean",
              "value": "true"
            },
            {
              "name": "limit",
              "value": "={{ $(\"Validate Watchlist and Build Actor Input\").first().json.configs.length }}"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000007",
      "name": "Normalize Current Creator Metrics",
      "type": "n8n-nodes-base.code",
      "position": [
        -848,
        80
      ],
      "parameters": {
        "jsCode": "\nconst rows = $input.all().map((item) => item.json).filter((item) => item && item.handle && !item.error);\nif (rows.length === 0) throw new Error('TikTok returned no public profiles. Check the handles or profile URLs and inspect the Actor run.');\nconst sheetsEpochOffset = 25569;\nconst serial = (value) => {\n  const date = new Date(value || Date.now());\n  return Number.isFinite(date.getTime()) ? date.getTime() / 86400000 + sheetsEpochOffset : null;\n};\nconst numberOrNull = (value) => Number.isFinite(Number(value)) ? Number(value) : null;\nconst formula = (url, label) => url ? '=HYPERLINK(\"' + String(url).replace(/\"/g, '\"\"') + '\",\"' + String(label).replace(/\"/g, '\"\"') + '\")' : '';\nconst stateLabel = (value, label) => value === true ? '\u2611 ' + label : '\u2715 No';\nreturn rows.map((profile) => {\n  const handleKey = String(profile.handle).trim().replace(/^@/, '').toLowerCase();\n  const profileUrl = String(profile.profileUrl || ('https://www.tiktok.com/@' + handleKey));\n  return { json: {\n    updatedAt: serial(profile.scrapedAt),\n    scrapedAtIso: new Date(profile.scrapedAt || Date.now()).toISOString(),\n    profileLink: formula(profileUrl, '@' + handleKey),\n    profileUrl,\n    handle: '@' + handleKey,\n    handleKey,\n    nickname: String(profile.nickname || ''),\n    bio: String(profile.signature || ''),\n    followers: numberOrNull(profile.followers),\n    following: numberOrNull(profile.following),\n    likes: numberOrNull(profile.likes),\n    videosCount: numberOrNull(profile.videosCount),\n    likesPerVideo: numberOrNull(profile.likesPerVideo),\n    followerFollowingRatio: numberOrNull(profile.followerFollowingRatio),\n    verified: stateLabel(profile.verified, 'Verified'),\n    privateAccount: stateLabel(profile.privateAccount, 'Private account'),\n    region: String(profile.region || ''),\n    language: String(profile.language || ''),\n    bioLink: formula(profile.bioLink, 'Open bio link'),\n    bioLinkUrl: String(profile.bioLink || ''),\n    commerceUser: stateLabel(profile.commerceUser, 'Commerce account'),\n    tiktokSeller: stateLabel(profile.tiktokSeller, 'TikTok seller'),\n    isLive: stateLabel(profile.isLive, 'Live now'),\n    userId: String(profile.userId || '')\n  } };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000008",
      "name": "Read Previous Creator Metrics",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -608,
        80
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "1783850914",
          "cachedResultName": "Current Profiles"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1YV806viFdmh0J6m2OuzZXhgDvrrWL8-GqU-DKh5ygKE",
          "cachedResultName": "FetchCat n8n QA - TikTok Profiles"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "executeOnce": true,
      "typeVersion": 4.7,
      "alwaysOutputData": true
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000009",
      "name": "Compare Growth and Qualification",
      "type": "n8n-nodes-base.code",
      "position": [
        -368,
        80
      ],
      "parameters": {
        "jsCode": "const current = $('Normalize Current Creator Metrics').all().map((item) => item.json);\nconst previousRows = $input.all().map((item) => item.json || {});\nconst configs = $('Validate Watchlist and Build Actor Input').first().json.configs;\nconst configByHandle = new Map(configs.map((entry) => [entry.handle, entry]));\nconst normalizeHandle = (value) => String(value || '').trim().replace(/^@/, '').toLowerCase();\nconst numberOrNull = (value) => value === '' || value === null || value === undefined || !Number.isFinite(Number(value)) ? null : Number(value);\nconst previousByHandle = new Map();\nfor (const row of previousRows) {\n  const handle = normalizeHandle(row['TikTok handle'] || row.Handle || row.handle);\n  if (handle) previousByHandle.set(handle, row);\n}\nconst round = (value, digits = 2) => value === null || !Number.isFinite(value) ? null : Number(value.toFixed(digits));\nconst rows = current.map((profile) => {\n  const config = configByHandle.get(profile.handleKey) || { label: '', minimumFollowers: 0, maximumFollowers: 1+1234567890, alertGrowthPercent: 5, alertFollowerGain: 1000 };\n  const previous = previousByHandle.get(profile.handleKey);\n  const previousFollowers = numberOrNull(previous?.Followers);\n  const previousLikes = numberOrNull(previous?.['Total likes']);\n  const previousVideos = numberOrNull(previous?.Videos);\n  const baseline = previousFollowers === null;\n  const followerChange = baseline || profile.followers === null ? null : profile.followers - previousFollowers;\n  const followerGrowthPercent = followerChange === null || previousFollowers <= 0 ? null : round((followerChange / previousFollowers) * 100);\n  const likesChange = previousLikes === null || profile.likes === null ? null : profile.likes - previousLikes;\n  const videoChange = previousVideos === null || profile.videosCount === null ? null : profile.videosCount - previousVideos;\n  let qualification = 'Qualified';\n  if (profile.followers === null) qualification = 'Needs review';\n  else if (profile.followers < config.minimumFollowers) qualification = 'Below minimum';\n  else if (profile.followers > config.maximumFollowers) qualification = 'Above maximum';\n  const previousQualification = String(previous?.Qualification || '');\n  const qualificationChanged = Boolean(previousQualification && previousQualification !== qualification);\n  const meaningfulGrowth = (followerGrowthPercent ?? 0) >= config.alertGrowthPercent || (followerChange ?? 0) >= config.alertFollowerGain;\n  const meaningfulDecline = (followerGrowthPercent ?? 0) <= -config.alertGrowthPercent;\n  const shouldAlert = baseline || meaningfulGrowth || meaningfulDecline || qualificationChanged;\n  let changeStatus = 'No material change';\n  if (baseline) changeStatus = 'Baseline created';\n  else if (meaningfulGrowth) changeStatus = 'Growth alert';\n  else if (meaningfulDecline) changeStatus = 'Decline alert';\n  else if (qualificationChanged) changeStatus = 'Qualification changed';\n  return {\n    ...profile,\n    label: config.label,\n    followerChange,\n    followerGrowthPercent,\n    likesChange,\n    videoChange,\n    qualification,\n    changeStatus,\n    shouldAlert,\n    snapshotKey: '@' + profile.handleKey + ' | ' + profile.scrapedAtIso.replace('T', ' ').replace(/\\.\\d{3}Z$/, ' UTC')\n  };\n});\nconst alertRows = rows.filter((row) => row.shouldAlert).sort((a, b) => (b.followerGrowthPercent ?? -Infinity) - (a.followerGrowthPercent ?? -Infinity));\nconst escapeHtml = (value) => String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\nconst formatNumber = (value) => value === null ? 'unavailable' : Number(value).toLocaleString('en-US');\nconst formatSigned = (value) => (value > 0 ? '+' : '') + formatNumber(value);\nconst formatPercent = (value) => value === null ? 'unavailable' : (value > 0 ? '+' : '') + value + '%';\nconst title = rows.every((row) => row.changeStatus === 'Baseline created') ? 'TikTok creator baseline created' : 'TikTok creator changes';\nconst lines = alertRows.slice(0, 10).map((row, index) => {\n  const label = row.label ? ' | ' + escapeHtml(row.label) : '';\n  const heading = (index + 1) + '. <a href=\"' + escapeHtml(row.profileUrl) + '\"><b>' + escapeHtml(row.handle) + '</b></a>' + label;\n  const totals = formatNumber(row.followers) + ' followers | ' + formatNumber(row.videosCount) + ' videos';\n  if (row.changeStatus === 'Baseline created') {\n    return heading + '\\n' + totals + '\\n' + escapeHtml(row.qualification) + ' | First snapshot saved. Future runs will show changes.';\n  }\n  return heading + '\\n' + totals + '\\nFollower change: ' + formatSigned(row.followerChange) + ' (' + formatPercent(row.followerGrowthPercent) + ') | Video change: ' +\n    formatSigned(row.videoChange) + '\\n' + escapeHtml(row.qualification) + ' | ' + escapeHtml(row.changeStatus);\n});\nconst telegramMessage = '<b>' + title + '</b>\\n\\n' + lines.join('\\n\\n') + (alertRows.length > 10 ? '\\n\\n+' + (alertRows.length - 10) + ' more changed creators in Google Sheets.' : '');\nreturn [{ json: { rows, shouldAlert: alertRows.length > 0, alertCount: alertRows.length, telegramMessage } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000010",
      "name": "Has Baseline or Meaningful Changes",
      "type": "n8n-nodes-base.if",
      "position": [
        -128,
        80
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "tiktok-monitor-alert",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.shouldAlert }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000011",
      "name": "3. Send Creator Changes to Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        112,
        -80
      ],
      "parameters": {
        "text": "={{ $json.telegramMessage }}",
        "chatId": "123456789",
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false,
          "disable_notification": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000012",
      "name": "Prepare Snapshot Rows",
      "type": "n8n-nodes-base.code",
      "position": [
        352,
        80
      ],
      "parameters": {
        "jsCode": "return [{ json: $('Compare Growth and Qualification').first().json }];"
      },
      "executeOnce": true,
      "typeVersion": 2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000013",
      "name": "Expand Snapshot History",
      "type": "n8n-nodes-base.code",
      "position": [
        592,
        80
      ],
      "parameters": {
        "jsCode": "return $json.rows.map((row) => ({ json: row }));"
      },
      "typeVersion": 2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000014",
      "name": "4. Append Snapshot History",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        832,
        80
      ],
      "parameters": {
        "columns": {
          "value": {
            "Name": "={{ $json.nickname }}",
            "Label": "={{ $json.label }}",
            "Videos": "={{ $json.videosCount }}",
            "Profile": "={{ $json.profileLink }}",
            "Followers": "={{ $json.followers }}",
            "Snapshot at": "={{ $json.updatedAt }}",
            "Total likes": "={{ $json.likes }}",
            "Snapshot key": "={{ $json.snapshotKey }}",
            "Change status": "={{ $json.changeStatus }}",
            "Qualification": "={{ $json.qualification }}",
            "TikTok handle": "={{ $json.handle }}",
            "Follower change": "={{ $json.followerChange }}",
            "Follower growth %": "={{ $json.followerGrowthPercent }}",
            "Total likes change": "={{ $json.likesChange }}",
            "Video count change": "={{ $json.videoChange }}"
          },
          "schema": [
            {
              "id": "Snapshot key",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Snapshot key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Snapshot at",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Snapshot at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Label",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Label",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Profile",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Profile",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "TikTok handle",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "TikTok handle",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Followers",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Followers",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Follower change",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Follower change",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Follower growth %",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Follower growth %",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total likes",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Total likes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total likes change",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Total likes change",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Videos",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Videos",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Video count change",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Video count change",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Qualification",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Qualification",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Change status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Change status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "1786791067",
          "cachedResultName": "Snapshot History"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1YV806viFdmh0J6m2OuzZXhgDvrrWL8-GqU-DKh5ygKE",
          "cachedResultName": "FetchCat n8n QA - TikTok Profiles"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000015",
      "name": "Continue After History",
      "type": "n8n-nodes-base.code",
      "position": [
        1072,
        80
      ],
      "parameters": {
        "jsCode": "return [{ json: $('Prepare Snapshot Rows').first().json }];"
      },
      "executeOnce": true,
      "typeVersion": 2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000016",
      "name": "Expand Current Profiles",
      "type": "n8n-nodes-base.code",
      "position": [
        1312,
        80
      ],
      "parameters": {
        "jsCode": "return $json.rows.map((row) => ({ json: row }));"
      },
      "typeVersion": 2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000017",
      "name": "5. Update Current Profiles",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1552,
        80
      ],
      "parameters": {
        "columns": {
          "value": {
            "Bio": "={{ $json.bio }}",
            "Name": "={{ $json.nickname }}",
            "Label": "={{ $json.label }}",
            "Region": "={{ $json.region }}",
            "Videos": "={{ $json.videosCount }}",
            "Profile": "={{ $json.profileLink }}",
            "Bio link": "={{ $json.bioLink }}",
            "Language": "={{ $json.language }}",
            "Verified": "={{ $json.verified }}",
            "Followers": "={{ $json.followers }}",
            "Following": "={{ $json.following }}",
            "Updated at": "={{ $json.updatedAt }}",
            "Total likes": "={{ $json.likes }}",
            "Change status": "={{ $json.changeStatus }}",
            "Qualification": "={{ $json.qualification }}",
            "TikTok handle": "={{ $json.handle }}",
            "Follower change": "={{ $json.followerChange }}",
            "Likes per video": "={{ $json.likesPerVideo }}",
            "Follower growth %": "={{ $json.followerGrowthPercent }}",
            "Total likes change": "={{ $json.likesChange }}",
            "Video count change": "={{ $json.videoChange }}"
          },
          "schema": [
            {
              "id": "Updated at",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Updated at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Label",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Label",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Profile",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Profile",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "TikTok handle",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "TikTok handle",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Bio",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Bio",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Followers",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Followers",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Follower change",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Follower change",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Follower growth %",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Follower growth %",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Following",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Following",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total likes",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Total likes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total likes change",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Total likes change",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Videos",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Videos",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Video count change",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Video count change",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Likes per video",
              "type": "number",
              "display": true,
              "required": false,
              "displayName": "Likes per video",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Verified",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Verified",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Region",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Region",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Language",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Language",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Bio link",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Bio link",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Qualification",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Qualification",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Change status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Change status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "TikTok handle"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "useAppend": true
        },
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "1783850914",
          "cachedResultName": "Current Profiles"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1YV806viFdmh0J6m2OuzZXhgDvrrWL8-GqU-DKh5ygKE",
          "cachedResultName": "FetchCat n8n QA - TikTok Profiles"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000018",
      "name": "Creator Monitor Complete",
      "type": "n8n-nodes-base.code",
      "position": [
        1792,
        80
      ],
      "parameters": {
        "jsCode": "const result = $('Compare Growth and Qualification').first().json;\nreturn [{ json: { status: 'complete', profilesUpdated: result.rows.length, alertsSent: result.alertCount } }];"
      },
      "executeOnce": true,
      "typeVersion": 2
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000030",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2720,
        -384
      ],
      "parameters": {
        "width": 480,
        "height": 1040,
        "content": "## Monitor TikTok Creator Growth\n\nTrack a reusable TikTok creator or competitor watchlist with `fetch_cat/tiktok-profile-scraper`, Google Sheets, and Telegram. The workflow works on n8n Cloud or self-hosted n8n and uses deterministic thresholds rather than an AI model.\n\n### How it works\n\n1. Starts manually or every Monday at 09:00.\n2. Reads active creators and alert thresholds from the Creators sheet.\n3. Scrapes every profile in one efficient FetchCat Actor run.\n4. Compares current followers, likes, videos, and qualification with the previous snapshot.\n5. Sends a Telegram baseline on the first run and later alerts only for meaningful growth, decline, or qualification changes.\n6. Appends dated history and updates the latest profile table after successful alert delivery.\n\n### Setup\n\n- [ ] Create the Creators, Current Profiles, and Snapshot History tabs with the documented headers.\n- [ ] Add handles, labels, qualification limits, and alert thresholds to Creators.\n- [ ] Select the same spreadsheet and appropriate tab in all Google Sheets nodes.\n- [ ] Connect one Apify HTTP Header Auth credential to both FetchCat request nodes.\n- [ ] Connect Telegram, run manually to create the baseline, then publish the schedule.\n\nRecent-video monitoring is intentionally excluded because TikTok exposes that data inconsistently."
      },
      "typeVersion": 1
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000031",
      "name": "Load Creator Watchlist",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2128,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 560,
        "content": "## Load the creator watchlist\n\nStarts manually or weekly and reads active handles, labels, qualification limits, and alert thresholds from Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000032",
      "name": "Run Batch Profile Check",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1616,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 560,
        "content": "## Run one profile batch\n\nValidates up to 500 unique creators, runs `fetch_cat/tiktok-profile-scraper` once, and downloads the current public profile metrics."
      },
      "typeVersion": 1
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000033",
      "name": "Compare with Previous Metrics",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -896,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 560,
        "content": "## Compare growth and qualification\n\nNormalizes current metrics, reads the previous profile table, and calculates follower, likes, video, and qualification changes."
      },
      "typeVersion": 1
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000034",
      "name": "Route Meaningful Alerts",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -176,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 640,
        "content": "## Route meaningful alerts\n\nSends a readable first-run baseline or significant changes to Telegram. Quiet runs continue without a notification."
      },
      "typeVersion": 1
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000035",
      "name": "Save Snapshot History",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 560,
        "content": "## Save dated history\n\nAppends one dated row per creator so users can sort, chart, and audit changes over time."
      },
      "typeVersion": 1
    },
    {
      "id": "61000000-0000-0000-4000-8000-000000000036",
      "name": "Update Current Profiles",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1264,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 560,
        "content": "## Update the latest profile table\n\nUpserts one current row per handle only after any required Telegram alert succeeds, then returns a concise execution summary."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "timezone": "Europe/Lisbon",
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "executionOrder": "v1",
    "saveManualExecutions": true
  },
  "versionId": "aea88acb-2a33-48e2-af55-7022023550f0",
  "nodeGroups": [],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "1. Read Creator Watchlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Snapshot Rows": {
      "main": [
        [
          {
            "node": "Expand Snapshot History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Monday Trigger": {
      "main": [
        [
          {
            "node": "1. Read Creator Watchlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Continue After History": {
      "main": [
        [
          {
            "node": "Expand Current Profiles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand Current Profiles": {
      "main": [
        [
          {
            "node": "5. Update Current Profiles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand Snapshot History": {
      "main": [
        [
          {
            "node": "4. Append Snapshot History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Read Creator Watchlist": {
      "main": [
        [
          {
            "node": "Validate Watchlist and Build Actor Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Append Snapshot History": {
      "main": [
        [
          {
            "node": "Continue After History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Update Current Profiles": {
      "main": [
        [
          {
            "node": "Creator Monitor Complete",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Previous Creator Metrics": {
      "main": [
        [
          {
            "node": "Compare Growth and Qualification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare Growth and Qualification": {
      "main": [
        [
          {
            "node": "Has Baseline or Meaningful Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Current TikTok Profiles": {
      "main": [
        [
          {
            "node": "Normalize Current Creator Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Current Creator Metrics": {
      "main": [
        [
          {
            "node": "Read Previous Creator Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Baseline or Meaningful Changes": {
      "main": [
        [
          {
            "node": "3. Send Creator Changes to Telegram",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Snapshot Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Send Creator Changes to Telegram": {
      "main": [
        [
          {
            "node": "Prepare Snapshot Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Run FetchCat TikTok Profile Scraper": {
      "main": [
        [
          {
            "node": "Download Current TikTok Profiles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Watchlist and Build Actor Input": {
      "main": [
        [
          {
            "node": "2. Run FetchCat TikTok Profile Scraper",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs manually or every Monday to monitor a TikTok creator watchlist by scraping profile metrics with Apify, logging snapshots in Google Sheets, and sending baseline or change alerts to Telegram. Runs on a manual trigger or on a weekly schedule every Monday at…

Source: https://n8n.io/workflows/17718/ — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

Google Maps Lead Gen - Apify to GSheet. Uses httpRequest, googleSheets, telegram. Event-driven trigger; 8 nodes.

HTTP Request, Google Sheets, Telegram
Web Scraping

This workflow runs hourly (or manually) to scrape posts from public Telegram channels via Apify, batch-scores them with OpenAI against an intelligence goal, saves qualified items to Google Sheets, and

Data Table, HTTP Request, OpenAI +2
Web Scraping

Telegram Lead Gen - Apify Google Maps to Sheets. Uses telegramTrigger, telegram, httpRequest, openAi. Event-driven trigger; 25 nodes.

Telegram Trigger, Telegram, HTTP Request +5
Web Scraping

A customized n8n workflow inspired by the Lead Generation Agent template. It automates B2B lead scraping via Apify, extracts contact emails with AI, sends cold emails via Gmail, and logs every interac

Telegram, Google Sheets, Gmail +4
Web Scraping

This powerful workflow automates the evaluation of new digital tools, websites, or platforms with the goal of assessing their potential impact on your business. By leveraging Telegram for user input,

Telegram Trigger, OpenRouter Chat, Telegram +6