AutomationFlowsEmail & Gmail › Monitor Tiktok Competitors with Scavio Ai, Google Sheets, and Gmail Alerts

Monitor Tiktok Competitors with Scavio Ai, Google Sheets, and Gmail Alerts

ByScavio AI @scavio-ai on n8n.io

Every 6 hours, reads competitor TikTok usernames from a Google Sheet. Resolves each username to a TikTok profile and fetches the 5 most recent posts via the Scavio API. Compares fetched posts against the last known video ID to detect new content. Logs every new post to a Posts…

Cron / scheduled trigger★★★★☆ complexity18 nodesGoogle SheetsN8N Nodes ScavioGmail
Email & Gmail Trigger: Cron / scheduled Nodes: 18 Complexity: ★★★★☆ Added:

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

This workflow follows the Gmail → Google Sheets 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
{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "TikTok Competitor Monitor with Scavio + Google Sheets",
  "nodes": [
    {
      "id": "sticky-master",
      "name": "How it works",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        180,
        160
      ],
      "parameters": {
        "width": 580,
        "height": 680,
        "content": "## TikTok Competitor Monitor with Scavio + Google Sheets\n\n### How it works\nThis workflow monitors competitor TikTok accounts for new posts. Every 6 hours it reads usernames from a Google Sheet, fetches their latest videos via Scavio, compares against the last known video ID, and logs every new post with its description, metrics, and direct link to a Posts tab. If new content is found, you get a Gmail alert.\n\nPowered by [Scavio](https://scavio.dev), a real-time search API for Google, Amazon, Walmart, YouTube, Reddit, and TikTok. Free tier: 250 credits per month.\n\n### Setup steps\n- [ ] Install the **Scavio** community node: Settings -> Community Nodes -> install `n8n-nodes-scavio`.\n- [ ] Get a free Scavio API key at https://dashboard.scavio.dev (250 credits/mo).\n- [ ] Create a Google Sheet with two tabs:\n  - **Competitors**: columns `username`, `sec_user_id` (leave blank), `last_video_id` (leave blank). Add one competitor per row.\n  - **Posts**: columns `username`, `description`, `views`, `likes`, `comments`, `shares`, `link`, `detected_at`.\n- [ ] Attach credentials: Scavio API on **Get Profile** + **Get Latest Posts**, Google Sheets OAuth2 on all sheet nodes, Gmail OAuth2 on **Alert new posts**.\n- [ ] Open sheet nodes and pick your spreadsheet/tabs.\n- [ ] Open **Alert new posts** and set `sendTo` to your email.\n- [ ] Activate the workflow.\n\n### Tips\n- The first run seeds `last_video_id` without alerting. Alerts start on the second run.\n- Each competitor costs 2 credits per run. 5 competitors every 6h fits the free tier.\n- Build charts in Google Sheets from the Posts tab to compare competitor posting frequency and engagement."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-feedback",
      "name": "Feedback",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        180,
        880
      ],
      "parameters": {
        "color": 4,
        "width": 580,
        "height": 100,
        "content": "### How can we improve this workflow?\n### [>>> Share your feedback at scavio.dev/feedback](https://scavio.dev)"
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-cluster-a",
      "name": "Cluster: Read competitors",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        840,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 280,
        "content": "## 1. Read competitors\n\nFires every 6 hours and reads every row from the \"Competitors\" tab (one row = one TikTok account to monitor)."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-cluster-b",
      "name": "Cluster: Check for new posts",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1400,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 1340,
        "height": 280,
        "content": "## 2. Check for new posts\n\nFor each competitor, resolves the sec_user_id via Get Profile, fetches the 5 newest posts, and compares the latest video_id against the stored last_video_id to detect new content."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-cluster-c",
      "name": "Cluster: Alert, log & save",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2780,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 960,
        "height": 500,
        "content": "## 3. Alert, log & save\n\nIf new posts are found, sends a Gmail alert, logs each post with description, metrics, and link to the Posts tab. Always saves sec_user_id and last_video_id back to the Competitors tab."
      },
      "typeVersion": 1
    },
    {
      "id": "1",
      "name": "Every 6 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        900,
        320
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "2",
      "name": "Read competitors",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1120,
        320
      ],
      "parameters": {
        "options": {},
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "Competitors"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "3",
      "name": "Loop competitors",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1460,
        320
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 3
    },
    {
      "id": "4",
      "name": "Throttle",
      "type": "n8n-nodes-base.wait",
      "position": [
        1680,
        320
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 2
      },
      "typeVersion": 1.1
    },
    {
      "id": "5",
      "name": "Get Profile",
      "type": "n8n-nodes-scavio.scavio",
      "position": [
        1900,
        320
      ],
      "parameters": {
        "resource": "tiktok",
        "username": "={{ $json.username }}",
        "operation": "getProfile"
      },
      "credentials": {
        "scavioApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "6",
      "name": "Store sec_user_id",
      "type": "n8n-nodes-base.code",
      "position": [
        2120,
        320
      ],
      "parameters": {
        "jsCode": "const profile = $json;\nconst row = $('Read competitors').item.json;\nconst secUid = profile.data?.user?.sec_uid ?? row.sec_user_id ?? '';\n\nreturn [{\n  json: {\n    username: row.username,\n    sec_user_id: secUid,\n    last_video_id: String(row.last_video_id ?? '').trim(),\n  },\n}];",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "7",
      "name": "Get Latest Posts",
      "type": "n8n-nodes-scavio.scavio",
      "position": [
        2340,
        320
      ],
      "parameters": {
        "resource": "tiktok",
        "operation": "getUserPosts",
        "sec_user_id": "={{ $json.sec_user_id }}",
        "additionalOptions": {
          "count": 5
        }
      },
      "credentials": {
        "scavioApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "8",
      "name": "Find new posts",
      "type": "n8n-nodes-base.code",
      "position": [
        2560,
        320
      ],
      "parameters": {
        "jsCode": "const prev = $('Store sec_user_id').item.json;\nconst postsResponse = $json;\nconst posts = postsResponse.data?.aweme_list ?? [];\nconst lastVideoId = prev.last_video_id;\nconst username = prev.username;\n\nif (!Array.isArray(posts) || posts.length === 0) {\n  return [{ json: { username, sec_user_id: prev.sec_user_id, last_video_id: lastVideoId, hasNewPosts: false, newPosts: [], newPostCount: 0 } }];\n}\n\nconst sorted = [...posts].sort((a, b) => Number(b.create_time ?? 0) - Number(a.create_time ?? 0));\nconst latestVideoId = String(sorted[0].aweme_id ?? '');\n\nconst newPosts = [];\nfor (const v of sorted) {\n  const vid = String(v.aweme_id ?? '');\n  if (lastVideoId && vid === lastVideoId) break;\n  const s = v.statistics ?? {};\n  newPosts.push({\n    video_id: vid,\n    description: String(v.desc ?? '').substring(0, 200),\n    views: Number(s.play_count ?? 0),\n    likes: Number(s.digg_count ?? 0),\n    comments: Number(s.comment_count ?? 0),\n    shares: Number(s.share_count ?? 0),\n    link: 'https://www.tiktok.com/@' + username + '/video/' + vid,\n  });\n}\n\nreturn [{\n  json: {\n    username,\n    sec_user_id: prev.sec_user_id,\n    last_video_id: latestVideoId,\n    hasNewPosts: newPosts.length > 0,\n    newPosts,\n    newPostCount: newPosts.length,\n  },\n}];",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "9",
      "name": "Has new posts?",
      "type": "n8n-nodes-base.if",
      "position": [
        2840,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.hasNewPosts }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "12",
      "name": "Format posts",
      "type": "n8n-nodes-base.code",
      "position": [
        3060,
        260
      ],
      "parameters": {
        "jsCode": "const data = $('Find new posts').item.json;\nconst now = new Date().toISOString();\n\nreturn data.newPosts.map(p => ({\n  json: {\n    username: data.username,\n    description: p.description,\n    views: p.views,\n    likes: p.likes,\n    comments: p.comments,\n    shares: p.shares,\n    link: p.link,\n    detected_at: now,\n  }\n}));",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "13",
      "name": "Log posts",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3280,
        260
      ],
      "parameters": {
        "columns": {
          "value": {
            "link": "={{ $json.link }}",
            "likes": "={{ $json.likes }}",
            "views": "={{ $json.views }}",
            "shares": "={{ $json.shares }}",
            "comments": "={{ $json.comments }}",
            "username": "={{ $json.username }}",
            "description": "={{ $json.description }}",
            "detected_at": "={{ $json.detected_at }}"
          },
          "schema": [
            {
              "id": "username",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "username",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "description",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "views",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "views",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "likes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "likes",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "comments",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "comments",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "shares",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "shares",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "link",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "link",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "detected_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "detected_at",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "Posts"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "10",
      "name": "Alert new posts",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3500,
        260
      ],
      "parameters": {
        "sendTo": "you@example.com",
        "message": "=@{{ $('Find new posts').item.json.username }} has {{ $('Find new posts').item.json.newPostCount }} new TikTok post{{ $('Find new posts').item.json.newPostCount > 1 ? 's' : '' }}:\n\n{{ $('Find new posts').item.json.newPosts.map(p => '- ' + p.description.substring(0, 80) + '\\n  Views: ' + p.views + ' | Likes: ' + p.likes + '\\n  ' + p.link).join('\\n\\n') }}\n\n-- Scavio competitor monitor",
        "options": {},
        "subject": "=TikTok: @{{ $('Find new posts').item.json.username }} posted {{ $('Find new posts').item.json.newPostCount }} new video{{ $('Find new posts').item.json.newPostCount > 1 ? 's' : '' }}",
        "emailType": "text",
        "operation": "send"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "11",
      "name": "Save state",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3280,
        480
      ],
      "parameters": {
        "columns": {
          "value": {
            "username": "={{ $('Find new posts').item.json.username }}",
            "sec_user_id": "={{ $('Find new posts').item.json.sec_user_id }}",
            "last_video_id": "={{ $('Find new posts').item.json.last_video_id }}"
          },
          "schema": [
            {
              "id": "username",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "username",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sec_user_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sec_user_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "last_video_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "last_video_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "username"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "Competitors"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Throttle": {
      "main": [
        [
          {
            "node": "Get Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log posts": {
      "main": [
        [
          {
            "node": "Alert new posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save state": {
      "main": [
        [
          {
            "node": "Loop competitors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Profile": {
      "main": [
        [
          {
            "node": "Store sec_user_id",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format posts": {
      "main": [
        [
          {
            "node": "Log posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 6 Hours": {
      "main": [
        [
          {
            "node": "Read competitors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find new posts": {
      "main": [
        [
          {
            "node": "Has new posts?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has new posts?": {
      "main": [
        [
          {
            "node": "Format posts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert new posts": {
      "main": [
        [
          {
            "node": "Save state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Latest Posts": {
      "main": [
        [
          {
            "node": "Find new posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop competitors": {
      "main": [
        [],
        [
          {
            "node": "Throttle",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read competitors": {
      "main": [
        [
          {
            "node": "Loop competitors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store sec_user_id": {
      "main": [
        [
          {
            "node": "Get Latest Posts",
            "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

Every 6 hours, reads competitor TikTok usernames from a Google Sheet. Resolves each username to a TikTok profile and fetches the 5 most recent posts via the Scavio API. Compares fetched posts against the last known video ID to detect new content. Logs every new post to a Posts…

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

More Email & Gmail workflows → · Browse all categories →

Related workflows

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

Email & Gmail

Track any number of Amazon products from a Google Sheet. Every 1,2,3... hours, the workflow: Reads your watchlist sheet (, , ) Loops through each product with a 2s throttle (Scavio API rate limit for

Google Sheets, N8N Nodes Scavio, Gmail
Email & Gmail

YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.

Gmail, Google Drive, Google Sheets +1
Email & Gmail

Looking for a way to track GitHub bounty issues automatically and get notified in real time? This GitHub Bounty Tracker workflow monitors repositories for issues labeled 💎 Bounty, logs them in Google

Google Sheets, HTTP Request, WhatsApp +1
Email & Gmail

This workflow automatically sends a beautifully designed HTML newsletter every Sunday at 8 AM, featuring products currently on sale from your Algolia-powered e-commerce store.

Google Sheets, HTTP Request, Gmail
Email & Gmail

This n8n template demonstrates how to build a Auto Lead Gen & Outreach System for Local Businesses specifically designed to help businesses that don’t have a website yet.

Google Sheets, HTTP Request, Google Drive +1