This workflow corresponds to n8n.io template #17676 — 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 →
{
"id": "5ZNr3c1pfy0x6T41",
"name": "Export TikTok Profiles to Google Sheets with Apify",
"tags": [],
"nodes": [
{
"id": "60000000-0000-0000-4000-8000-000000000001",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1568,
256
],
"parameters": {},
"typeVersion": 1
},
{
"id": "60000000-0000-0000-4000-8000-000000000008",
"name": "Daily 09:00 Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-1568,
64
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 9
}
]
}
},
"typeVersion": 1.3
},
{
"id": "60000000-0000-0000-4000-8000-000000000002",
"name": "1. Enter TikTok Profiles",
"type": "n8n-nodes-base.set",
"position": [
-1296,
192
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "tiktok-profiles",
"name": "profiles",
"type": "string",
"value": "selenagomez\ntherock\nbillieeilish"
},
{
"id": "tiktok-limit",
"name": "Maximum Profiles Per Run",
"type": "number",
"value": 25
}
]
}
},
"typeVersion": 3.4
},
{
"id": "60000000-0000-0000-4000-8000-000000000003",
"name": "Validate Profiles and Build Actor Input",
"type": "n8n-nodes-base.code",
"position": [
-1056,
192
],
"parameters": {
"jsCode": "const input = $input.first()?.json || {};\nconst raw = String(input.profiles || '').split(/[\\n,]+/).map((value) => value.trim()).filter(Boolean);\nconst maxProfiles = Math.max(1, Math.min(500, Math.floor(Number(input['Maximum Profiles Per Run']) || 25)));\nif (raw.length === 0) throw new Error('Enter at least one TikTok handle or profile URL.');\nconst handles = [];\nconst profileUrls = [];\nconst seen = new Set();\nfor (const value of raw) {\n const urlMatch = value.match(/(?:https?:\\/\\/)?(?:www\\.)?tiktok\\.com\\/@([A-Za-z0-9._-]+)/i);\n const handle = (urlMatch?.[1] || value.replace(/^@/, '')).trim();\n if (!/^[A-Za-z0-9._-]{2,64}$/.test(handle)) throw new Error('Invalid TikTok profile: ' + value);\n const key = handle.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n if (urlMatch) profileUrls.push({ url: 'https://www.tiktok.com/@' + handle });\n else handles.push(handle);\n}\nif (seen.size > maxProfiles) throw new Error('You entered ' + seen.size + ' unique profiles but Maximum Profiles Per Run is ' + maxProfiles + '.');\nreturn [{ json: { maxProfiles, actorInput: { handles, profileUrls, maxProfiles, includeRecentVideos: false, maxVideosPerProfile: 0, maxConcurrency: 3, runTimeSecs: 180, proxyConfiguration: { useApifyProxy: true } } } }];"
},
"typeVersion": 2
},
{
"id": "60000000-0000-0000-4000-8000-000000000004",
"name": "2. Run FetchCat TikTok Profile Scraper",
"type": "n8n-nodes-base.httpRequest",
"position": [
-816,
192
],
"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": "60000000-0000-0000-4000-8000-000000000005",
"name": "Download TikTok Profile Results",
"type": "n8n-nodes-base.httpRequest",
"position": [
-576,
192
],
"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 Profiles and Build Actor Input\").first().json.maxProfiles }}"
}
]
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.3
},
{
"id": "60000000-0000-0000-4000-8000-000000000006",
"name": "Prepare Human-Readable Profile Rows",
"type": "n8n-nodes-base.code",
"position": [
-336,
192
],
"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": "60000000-0000-0000-4000-8000-000000000007",
"name": "3. Save Profiles to Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
-96,
192
],
"parameters": {
"columns": {
"value": {
"Bio": "={{ $json.bio }}",
"Name": "={{ $json.nickname }}",
"Region": "={{ $json.region }}",
"Videos": "={{ $json.videosCount }}",
"Profile": "={{ $json.profileLink }}",
"User ID": "={{ $json.userId }}",
"Bio link": "={{ $json.bioLink }}",
"Language": "={{ $json.language }}",
"Live now": "={{ $json.isLive }}",
"Verified": "={{ $json.verified }}",
"Followers": "={{ $json.followers }}",
"Following": "={{ $json.following }}",
"Updated at": "={{ $json.updatedAt }}",
"Total likes": "={{ $json.likes }}",
"TikTok handle": "={{ $json.handle }}",
"TikTok seller": "={{ $json.tiktokSeller }}",
"Likes per video": "={{ $json.likesPerVideo }}",
"Private account": "={{ $json.privateAccount }}",
"Commerce account": "={{ $json.commerceUser }}",
"Follower/following ratio": "={{ $json.followerFollowingRatio }}"
},
"schema": [
{
"id": "Updated at",
"type": "number",
"display": true,
"required": false,
"displayName": "Updated at",
"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": "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": "Videos",
"type": "number",
"display": true,
"required": false,
"displayName": "Videos",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Likes per video",
"type": "number",
"display": true,
"required": false,
"displayName": "Likes per video",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Follower/following ratio",
"type": "number",
"display": true,
"required": false,
"displayName": "Follower/following ratio",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Verified",
"type": "string",
"display": true,
"required": false,
"displayName": "Verified",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Private account",
"type": "string",
"display": true,
"required": false,
"displayName": "Private account",
"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": "Commerce account",
"type": "string",
"display": true,
"required": false,
"displayName": "Commerce account",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "TikTok seller",
"type": "string",
"display": true,
"required": false,
"displayName": "TikTok seller",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Live now",
"type": "string",
"display": true,
"required": false,
"displayName": "Live now",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "User ID",
"type": "string",
"display": true,
"required": false,
"displayName": "User ID",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"TikTok handle"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"useAppend": true
},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "id",
"value": "186414753",
"cachedResultName": "TikTok Profiles"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "1a53w6kA5EM0iPSIZ-EC1-lsPRlkrx2vk6ZK7mgLIeaI",
"cachedResultName": "TikTok Profiles to Google Sheets - n8n Template"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "60000000-0000-0000-4000-8000-000000000020",
"name": "Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2208,
-304
],
"parameters": {
"width": 480,
"height": 900,
"content": "## Export TikTok Profiles to Google Sheets\n\nCollect current public TikTok profile data with `fetch_cat/tiktok-profile-scraper` and save one clean, sortable row per creator. This workflow works on n8n Cloud or self-hosted n8n, uses built-in nodes, and does not require OpenAI.\n\n### How it works\n\n1. Starts manually or every day at 09:00 and reads handles or full TikTok profile URLs from one editable Set node.\n2. Validates and deduplicates the list before spending Actor credits.\n3. Runs the FetchCat TikTok Profile Scraper once for the complete batch.\n4. Converts public metrics and account signals into human-readable columns.\n5. Updates Google Sheets by TikTok handle, so reruns refresh existing rows instead of creating duplicates.\n\n### Setup\n\n- [ ] [Open the public spreadsheet template](https://docs.google.com/spreadsheets/d/11sXzrv9g7rECryFt6HkWcXQxh7hpyHteuRT-cxcQZrE/edit?usp=drive_link), choose File > Make a copy, and save the copy in your Google Drive.\n- [ ] Paste up to 500 handles or profile URLs into 1. Enter TikTok Profiles.\n- [ ] Create an Apify HTTP Header Auth credential and select it in both FetchCat request nodes.\n- [ ] Connect Google Sheets and select your copied spreadsheet and its TikTok Profiles tab in 3. Save Profiles to Google Sheets.\n- [ ] Run manually and inspect the resulting profile table, then publish the workflow to enable the daily schedule.\n\nRecent videos are intentionally disabled because TikTok does not expose them consistently."
},
"typeVersion": 1
},
{
"id": "60000000-0000-0000-4000-8000-000000000021",
"name": "Trigger Workflow",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1616,
-176
],
"parameters": {
"color": 7,
"height": 608,
"content": "## Trigger workflow\n\nRun manually while setting up and testing, or publish the workflow to run it every day at 09:00."
},
"typeVersion": 1
},
{
"id": "60000000-0000-0000-4000-8000-000000000022",
"name": "Profile Input and Validation",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1344,
-48
],
"parameters": {
"color": 7,
"width": 432,
"height": 416,
"content": "## Profile input and validation\n\nEnter TikTok handles or full profile URLs, choose a batch limit, and validate and deduplicate the list before using Actor credits."
},
"typeVersion": 1
},
{
"id": "60000000-0000-0000-4000-8000-000000000023",
"name": "Scrape and Download Data",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
-48
],
"parameters": {
"color": 7,
"width": 432,
"height": 416,
"content": "## Scrape and download data\n\nRun `fetch_cat/tiktok-profile-scraper` once for the complete batch, wait for completion, and download the resulting public profile dataset."
},
"typeVersion": 1
},
{
"id": "60000000-0000-0000-4000-8000-000000000024",
"name": "Process and Save to Sheets",
"type": "n8n-nodes-base.stickyNote",
"position": [
-384,
-48
],
"parameters": {
"color": 7,
"width": 432,
"height": 416,
"content": "## Process and save to Sheets\n\nCreate human-readable dates, metrics, links, and account labels, then update Google Sheets using TikTok handle as the unique key."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"timezone": "Europe/Lisbon",
"binaryMode": "separate",
"callerPolicy": "workflowsFromSameOwner",
"executionOrder": "v1",
"saveManualExecutions": true
},
"versionId": "ec02723b-cdee-45a3-8d83-53f9cc6a619c",
"nodeGroups": [],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "1. Enter TikTok Profiles",
"type": "main",
"index": 0
}
]
]
},
"Daily 09:00 Trigger": {
"main": [
[
{
"node": "1. Enter TikTok Profiles",
"type": "main",
"index": 0
}
]
]
},
"1. Enter TikTok Profiles": {
"main": [
[
{
"node": "Validate Profiles and Build Actor Input",
"type": "main",
"index": 0
}
]
]
},
"Download TikTok Profile Results": {
"main": [
[
{
"node": "Prepare Human-Readable Profile Rows",
"type": "main",
"index": 0
}
]
]
},
"Prepare Human-Readable Profile Rows": {
"main": [
[
{
"node": "3. Save Profiles to Google Sheets",
"type": "main",
"index": 0
}
]
]
},
"2. Run FetchCat TikTok Profile Scraper": {
"main": [
[
{
"node": "Download TikTok Profile Results",
"type": "main",
"index": 0
}
]
]
},
"Validate Profiles 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.
googleSheetsOAuth2ApihttpHeaderAuth
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 daily at 09:00, scrapes public TikTok profile data via the Apify FetchCat TikTok Profile Scraper actor, and writes a clean, deduplicated table of metrics and account signals to Google Sheets, updating existing rows by handle. Runs on a manual…
Source: https://n8n.io/workflows/17676/ — 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.
04 - Firecrawl URL List Mini-Batch to Resilient Analyzer. Uses googleSheets, httpRequest, executeWorkflowTrigger. Event-driven trigger; 42 nodes.
Automate LinkedIn lead generation by scraping comments from targeted posts and enriching profiles with detailed data
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 T
This automated n8n workflow scrapes job listings from Upwork using Apify, processes and cleans the data, and generates daily email reports with job summaries. The system uses Google Sheets for data st
Transform LinkedIn profile URLs into comprehensive enriched lead profiles, quickly and automatically.