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 →
{
"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.
gmailOAuth2googleSheetsOAuth2ApiscavioApi
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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
YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.
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
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.
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.