This workflow follows the Googlegemini → 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 →
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
2000,
1408
],
"id": "484ca989-a86d-4479-a08f-877415229a79",
"name": "Manual Test Trigger"
},
{
"parameters": {
"httpMethod": "POST",
"path": "phase3-start",
"options": {
"responseData": "{\"ok\":true}"
}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
1984,
1600
],
"id": "62118ed1-25e5-4cd3-a16a-5bbf0aa60d7c",
"name": "Phase 3 Start Webhook"
},
{
"parameters": {
"authentication": "serviceAccount",
"documentId": {
"__rl": true,
"value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
"mode": "list",
"cachedResultName": "GOOGLE_SHEET_ID",
"cachedResultUrl": ""
},
"sheetName": {
"__rl": true,
"value": "Threads",
"mode": "name"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
2256,
1408
],
"id": "e9666cce-99d9-47d6-a239-fc82642dd6c1",
"name": "Read Threads Sheet",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "function getWebhookPayload() {\n try {\n const webhookItem = $('Phase 3 Start Webhook').first();\n return webhookItem.json.body || webhookItem.json || {};\n } catch (error) {\n return {};\n }\n}\n\nconst payload = getWebhookPayload();\nconst targetId = String(payload.id || payload.ID || '').trim();\nconst targetStatus = String(payload.status || payload.Status || '').trim().toLowerCase();\nconst targetScript = String(payload.narrator_script || payload.Narrator_Script || '').trim();\nconst targetAudio = String(payload.audio_path || payload.Audio_Path || '').trim();\n\nif (targetId && targetStatus === 'in progress' && targetScript && !targetAudio) {\n return [{\n json: {\n ID: targetId,\n Narrator_Script: payload.Narrator_Script || payload.narrator_script || '',\n Extracted_Content: payload.Extracted_Content || payload.extracted_content || '',\n Status: payload.Status || payload.status || 'In Progress',\n Note: payload.Note || payload.note || '',\n },\n }];\n}\n\nconst rows = $input.all().filter(item => {\n const id = String(item.json.ID || '').trim();\n const status = String(item.json.Status || '').trim().toLowerCase();\n const hasScript = String(item.json.Narrator_Script || '').trim();\n const hasAudio = String(item.json.Audio_Path || '').trim();\n if (targetId && id !== targetId) return false;\n return status === 'in progress' && hasScript && !hasAudio;\n});\n\nreturn rows.slice(0, 1);"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2512,
1168
],
"id": "80855357-a1e8-4f8b-8021-7fbe5242f106",
"name": "Pick Voice Row"
},
{
"parameters": {
"jsCode": "function getTargetId() {\n try {\n const webhookItem = $('Phase 3 Start Webhook').first();\n const payload = webhookItem.json.body || webhookItem.json || {};\n return String(payload.id || '').trim();\n } catch (error) {\n return '';\n }\n}\n\nconst targetId = getTargetId();\nconst rows = $input.all().filter(item => {\n const id = String(item.json.ID || '').trim();\n const status = String(item.json.Status || '').trim().toLowerCase();\n const hasScreenshots = String(item.json.Screenshots || '').trim();\n const hasAudio = String(item.json.Audio_Path || '').trim();\n const hasTiming = String(item.json.Audio_Timing || '').trim();\n const hasVisual = String(item.json.Visual_Video_Path || '').trim();\n if (targetId && id !== targetId) return false;\n return status === 'in progress' && hasScreenshots && hasAudio && hasTiming && !hasVisual;\n});\n\nreturn rows.slice(0, 1);"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2512,
1328
],
"id": "859890d7-e640-49d8-8bac-23a20b419231",
"name": "Pick Visual Row"
},
{
"parameters": {
"jsCode": "function getTargetId() {\n try {\n const webhookItem = $('Phase 3 Start Webhook').first();\n const payload = webhookItem.json.body || webhookItem.json || {};\n return String(payload.id || '').trim();\n } catch (error) {\n return '';\n }\n}\n\nconst targetId = getTargetId();\nconst rows = $input.all().filter(item => {\n const id = String(item.json.ID || '').trim();\n const status = String(item.json.Status || '').trim().toLowerCase();\n const audioPath = String(item.json.Audio_Path || '').trim();\n const visualPath = String(item.json.Visual_Video_Path || '').trim();\n const finalPath = String(item.json.Video_Path || '').trim();\n if (targetId && id !== targetId) return false;\n return status === 'in progress' && audioPath && visualPath && !finalPath;\n});\n\nreturn rows.slice(0, 1);"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3360,
1600
],
"id": "b618de55-5d33-450e-81e2-884fc68b7968",
"name": "Pick Merge Row"
},
{
"parameters": {
"method": "POST",
"url": "http://host.docker.internal:8000/phase3/voice",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { id: $json.ID, script: $json.Narrator_Script, extracted_content: $json.Extracted_Content || '' } }}",
"options": {
"timeout": 900000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
2768,
1168
],
"id": "6509f73e-66e7-42aa-8d99-bf4f648ac36b",
"name": "Run Voice Generator",
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 30000
},
{
"parameters": {
"method": "POST",
"url": "http://host.docker.internal:8000/phase3/visual",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { id: $json.ID, screenshots: $json.Screenshots, script: $json.Narrator_Script || '', audio_path: $json.Audio_Path || '', audio_timing: $json.Audio_Timing || '', extracted_content: $json.Extracted_Content || '' } }}",
"options": {
"timeout": 900000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
2768,
1328
],
"id": "f2a50f98-89f4-425a-9bbe-2dcf1af831de",
"name": "Run Visual Builder",
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 30000
},
{
"parameters": {
"method": "POST",
"url": "http://host.docker.internal:8000/phase3/merge",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { id: $json.ID, audio_path: $json.Audio_Path, visual_path: $json.Visual_Video_Path, script: $json.Narrator_Script || '', extracted_content: $json.Extracted_Content || '' } }}",
"options": {
"timeout": 1200000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
3600,
1600
],
"id": "3c2a893c-cfd2-41ff-a6fe-8886138a13f7",
"name": "Run Merge Final",
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 30000
},
{
"parameters": {
"authentication": "serviceAccount",
"operation": "appendOrUpdate",
"documentId": {
"__rl": true,
"value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
"mode": "list",
"cachedResultName": "GOOGLE_SHEET_ID",
"cachedResultUrl": ""
},
"sheetName": {
"__rl": true,
"value": "Threads",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"ID": "={{ $json.ID }}",
"Audio_Path": "={{ $json.Audio_Path }}",
"Audio_Timing": "={{ $json.Audio_Timing }}",
"Status": "={{ $json.Status }}",
"Note": "={{ $json.Note }}"
},
"matchingColumns": [
"ID"
],
"schema": [
{
"id": "ID",
"displayName": "ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Source_URL",
"displayName": "Source_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Collected_At",
"displayName": "Collected_At",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Status",
"displayName": "Status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Screenshots",
"displayName": "Screenshots",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Extracted_Content",
"displayName": "Extracted_Content",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Narrator_Script",
"displayName": "Narrator_Script",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Audio_Path",
"displayName": "Audio_Path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Audio_Timing",
"displayName": "Audio_Timing",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Visual_Video_Path",
"displayName": "Visual_Video_Path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Video_Path",
"displayName": "Video_Path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Caption",
"displayName": "Caption",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Admin_Decision",
"displayName": "Admin_Decision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "TikTok_Publish_ID",
"displayName": "TikTok_Publish_ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Published_URL",
"displayName": "Published_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Note",
"displayName": "Note",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Draft_Video_URL",
"displayName": "Draft_Video_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Draft_Drive_File_ID",
"displayName": "Draft_Drive_File_ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
3024,
1168
],
"id": "16232393-d513-44c2-a56b-519bc5330f1e",
"name": "Update Voice Row",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"authentication": "serviceAccount",
"operation": "appendOrUpdate",
"documentId": {
"__rl": true,
"value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
"mode": "list",
"cachedResultName": "GOOGLE_SHEET_ID",
"cachedResultUrl": ""
},
"sheetName": {
"__rl": true,
"value": "Threads",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"ID": "={{ $json.ID }}",
"Visual_Video_Path": "={{ $json.Visual_Video_Path }}",
"Status": "={{ $json.Status }}",
"Note": "={{ $json.Note }}"
},
"matchingColumns": [
"ID"
],
"schema": [
{
"id": "ID",
"displayName": "ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Source_URL",
"displayName": "Source_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Collected_At",
"displayName": "Collected_At",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Status",
"displayName": "Status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Screenshots",
"displayName": "Screenshots",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Extracted_Content",
"displayName": "Extracted_Content",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Narrator_Script",
"displayName": "Narrator_Script",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Audio_Path",
"displayName": "Audio_Path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Audio_Timing",
"displayName": "Audio_Timing",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Visual_Video_Path",
"displayName": "Visual_Video_Path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Video_Path",
"displayName": "Video_Path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Caption",
"displayName": "Caption",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Admin_Decision",
"displayName": "Admin_Decision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "TikTok_Publish_ID",
"displayName": "TikTok_Publish_ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Published_URL",
"displayName": "Published_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Note",
"displayName": "Note",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Draft_Video_URL",
"displayName": "Draft_Video_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Draft_Drive_File_ID",
"displayName": "Draft_Drive_File_ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
2976,
1328
],
"id": "e253b616-537b-48c1-b69d-05bcb4061dd3",
"name": "Update Visual Row",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"authentication": "serviceAccount",
"operation": "appendOrUpdate",
"documentId": {
"__rl": true,
"value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
"mode": "list",
"cachedResultName": "GOOGLE_SHEET_ID",
"cachedResultUrl": ""
},
"sheetName": {
"__rl": true,
"value": "Threads",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"ID": "={{ $json.ID }}",
"Video_Path": "={{ $json.Video_Path }}",
"Caption": "={{ $json.Caption }}",
"Status": "={{ $json.Status }}",
"Note": "={{ $json.Note }}"
},
"matchingColumns": [
"ID"
],
"schema": [
{
"id": "ID",
"displayName": "ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Source_URL",
"displayName": "Source_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Collected_At",
"displayName": "Collected_At",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Status",
"displayName": "Status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Screenshots",
"displayName": "Screenshots",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Extracted_Content",
"displayName": "Extracted_Content",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Narrator_Script",
"displayName": "Narrator_Script",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Audio_Path",
"displayName": "Audio_Path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Audio_Timing",
"displayName": "Audio_Timing",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Visual_Video_Path",
"displayName": "Visual_Video_Path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Video_Path",
"displayName": "Video_Path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Caption",
"displayName": "Caption",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Admin_Decision",
"displayName": "Admin_Decision",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "TikTok_Publish_ID",
"displayName": "TikTok_Publish_ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Published_URL",
"displayName": "Published_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Note",
"displayName": "Note",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Draft_Video_URL",
"displayName": "Draft_Video_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Draft_Drive_File_ID",
"displayName": "Draft_Drive_File_ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
4672,
1584
],
"id": "c5a26d52-8da3-4d56-86ac-d65fcea68f3c",
"name": "Update Merge Row",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.N8N_PHASE4_START_URL || 'http://n8n:5678/webhook/phase4-start' }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { id: $json.ID, video_path: $json.Video_Path || '', caption: $json.Caption || '', status: $json.Status, note: $json.Note || '', draft_video_url: $json.Draft_Video_URL || '', tiktok_publish_id: $json.TikTok_Publish_ID || '' } }}",
"options": {
"timeout": 30000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
4880,
1584
],
"id": "f03ad121-8d53-4f00-ada2-e3c9251c93d5",
"name": "Trigger Phase 4",
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 5000
},
{
"parameters": {
"authentication": "serviceAccount",
"documentId": {
"__rl": true,
"value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
"mode": "list",
"cachedResultName": "GOOGLE_SHEET_ID",
"cachedResultUrl": ""
},
"sheetName": {
"__rl": true,
"value": "Threads",
"mode": "name"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
3136,
1600
],
"id": "33cbcf97-c1e1-47bb-a6d0-df7daede6dbc",
"name": "Read Threads Sheet For Merge",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"authentication": "serviceAccount",
"documentId": {
"__rl": true,
"value": "1mRIuWxQGfkS29TCr3f9vfsfPZZ1XgNtWqhtu-PcXsfg",
"mode": "list",
"cachedResultName": "GOOGLE_SHEET_ID",
"cachedResultUrl": ""
},
"sheetName": {
"__rl": true,
"value": "Threads",
"mode": "name"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
3136,
1456
],
"id": "7a8a34aa-1b95-4ad0-bf78-81c1135ea9ea",
"name": "Read Threads Sheet For Merge After Visual",
"credentials": {
"googleApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const env = (typeof process !== 'undefined' && process.env) ? process.env : {};\nconst enabledRaw = String(env.AI_CAPTION_ENABLED || env.CAPTION_REWRITE_ENABLED || 'true').trim().toLowerCase();\nconst enabled = ['1', 'true', 'yes', 'on'].includes(enabledRaw);\nconst model = String(env.AI_CAPTION_MODEL || env.SCRIPT_REWRITE_MODEL || 'gemini-2.5-flash-lite').trim();\nfunction cleanText(value) { return String(value || '').replace(/\\s+/g, ' ').trim(); }\nfunction parseJsonObject(value) { try { const parsed = JSON.parse(String(value || '').trim() || '{}'); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; } catch (error) { return {}; } }\nfunction takeTexts(values, limit = 8) { const output = []; const seen = new Set(); for (const item of Array.isArray(values) ? values : []) { const raw = typeof item === 'string' ? item : (item && item.text); const text = cleanText(raw).slice(0, 500); const key = text.toLowerCase().replace(/[^0-9a-z\\u00c0-\\u1ef9]+/g, ''); if (!text || !key || seen.has(key)) continue; seen.add(key); output.push(text); if (output.length >= limit) break; } return output; }\nfor (const item of $input.all()) { const source = item.json; const extracted = parseJsonObject(source.Extracted_Content || ''); const script = cleanText(source.Narrator_Script || ''); const post = cleanText(extracted.post_text || ''); const segments = takeTexts(extracted.segments || [], 10); const comments = takeTexts(extracted.comments || [], 5); const fallbackCaption = cleanText(source.Caption || '') || 'Cau chuyen nay dang duoc ban luan tren Threads\\n\\n#threads #storytime #chuyencuocsong #xuhuong'; source._caption = { enabled, model, prompt: ['You are a Vietnamese TikTok caption strategist.', 'Read the narrator script and source context, then write a natural TikTok caption that teases the story without copying a random script segment.', 'The caption must be in Vietnamese, conversational, concise, and suitable for a story/discussion video sourced from Threads.', 'Do not invent facts. Do not mention AI, automation, screenshots, or internal workflow details.', 'Avoid generic ending questions like Ban nghi sao unless it truly fits the content.', 'Return valid JSON only with this schema: {\"caption\":\"...\",\"hashtags\":[\"#...\",\"#...\"]}.', 'Caption target: 1 to 3 short sentences, max 260 characters before hashtags.', 'Hashtags target: 4 to 7 hashtags. Use specific topic hashtags when clear, then add generic ones such as #threads #storytime #chuyencuocsong #xuhuong.'].join(' '), script, post, segments, comments, fallback_caption: fallbackCaption }; }\nreturn $input.all();"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3824,
1600
],
"id": "a3007880-3d07-46a6-a110-694f31e77f07",
"name": "Prepare AI Caption"
},
{
"parameters": {
"modelId": {
"__rl": true,
"mode": "id",
"value": "={{ $json._caption.model ? 'models/' + $json._caption.model.replace(/^models\\//, '') : 'models/gemini-2.5-flash-lite' }}"
},
"messages": {
"values": [
{
"content": "={{ $json._caption.prompt + '\\n\\nNARRATOR_SCRIPT:\\n' + ($json._caption.script || '(none)') + '\\n\\nPOST_TEXT:\\n' + ($json._caption.post || '(none)') + '\\n\\nSOURCE_SEGMENTS:\\n- ' + ((($json._caption.segments || []).length ? $json._caption.segments : ['(none)']).join('\\n- ')) + '\\n\\nCOMMENTS:\\n- ' + ((($json._caption.comments || []).length ? $json._caption.comments : ['(none)']).join('\\n- ')) + '\\n\\nFALLBACK_CAPTION:\\n' + ($json._caption.fallback_caption || '') + '\\n\\nReturn JSON only.' }}"
}
]
},
"simplify": false,
"jsonOutput": true,
"builtInTools": {},
"options": {
"maxOutputTokens": 800,
"temperature": 0.65,
"topP": 0.9
}
},
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1.1,
"position": [
4000,
1408
],
"id": "65eebbe5-adea-406d-aac4-b57c1a6c855b",
"name": "Gemini AI Caption Request",
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineByPosition",
"options": {}
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
4272,
1584
],
"id": "c3f2c053-50fe-48d4-be2d-2bf89ddbdde4",
"name": "Merge AI Caption Result"
},
{
"parameters": {
"jsCode": "const DEFAULT_HASHTAGS = ['#threads', '#storytime', '#chuyencuocsong', '#xuhuong'];\nfunction cleanText(value) { return String(value || '').replace(/\\s+/g, ' ').trim(); }\nfunction stripMarkdown(value) { const fence = String.fromCharCode(96).repeat(3); let text = cleanText(value); if (text.toLowerCase().startsWith(fence + 'json')) text = text.slice(7); else if (text.startsWith(fence)) text = text.slice(3); if (text.endsWith(fence)) text = text.slice(0, -3); return text.trim(); }\nfunction parseJsonCandidate(text) { const cleaned = stripMarkdown(text); if (!cleaned) return null; const candidates = [cleaned]; const braceMatches = cleaned.match(/\\{[\\s\\S]*\\}/g) || []; candidates.push(...braceMatches); for (const candidate of candidates) { try { const parsed = JSON.parse(candidate); if (parsed && typeof parsed === 'object') return parsed; } catch (error) {} } return null; }\nfunction extractTextDeep(value, bucket = []) { if (typeof value === 'string') { const text = cleanText(value); if (text) bucket.push(text); return bucket; } if (!value || typeof value !== 'object') return bucket; if (Array.isArray(value)) { value.forEach((entry) => extractTextDeep(entry, bucket)); return bucket; } for (const key of ['caption', 'text', 'output', 'generatedText', 'content', 'message']) { if (typeof value[key] === 'string') bucket.push(value[key]); } Object.values(value).forEach((entry) => { if (entry && typeof entry === 'object') extractTextDeep(entry, bucket); }); return bucket; }\nfunction extractCaptionPayload(data) { const direct = data && typeof data === 'object' ? data : {}; if (typeof direct.caption === 'string') return direct; for (const text of extractTextDeep(direct, [])) { const parsed = parseJsonCandidate(text); if (parsed && typeof parsed.caption === 'string') return parsed; } return {}; }\nfunction normalizeCaption(value) { let text = cleanText(value).replace(/^caption\\s*:\\s*/i, '').replace(/[\"'_*~|]+/g, ' ').replace(/(^|\\s)#[^\\s#]+/g, ' '); text = cleanText(text); if (text.length > 320) text = cleanText(text.slice(0, 320).replace(/\\s+\\S*$/, '')); return text; }\nfunction normalizeHashtag(value) { let tag = cleanText(value).toLowerCase(); tag = tag.replace(/[.,;:!?()[\\]{}\"'_*~|]/g, ''); tag = tag.replace(/\\s+/g, ''); if (!tag) return ''; if (!tag.startsWith('#')) tag = '#' + tag; tag = tag.replace(/[^#0-9a-z_\\u00c0-\\u1ef9]/g, ''); return tag.length > 1 ? tag : ''; }\nfunction normalizeHashtags(values) { const raw = Array.isArray(values) ? values : String(values || '').split(/\\s+/); const result = []; const seen = new Set(); for (const value of [...raw, ...DEFAULT_HASHTAGS]) { const tag = normalizeHashtag(value); if (!tag || seen.has(tag)) continue; seen.add(tag); result.push(tag); if (result.length >= 7) break; } return result; }\nfor (const item of $input.all()) { const meta = item.json._caption || {}; const note = cleanText(item.json.Note || ''); const fallback = cleanText(meta.fallback_caption || item.json.Caption || ''); const disabled = !meta.enabled; const payload = extractCaptionPayload(item.json); const generatedCaption = normalizeCaption(payload.caption || ''); const hashtags = normalizeHashtags(payload.hashtags || payload.hashtag || ''); if (disabled) { item.json.Caption = fallback; item.json.Note = cleanText(note + ' | AI caption skipped: disabled by AI_CAPTION_ENABLED'); } else if (generatedCaption && generatedCaption.length >= 20) { item.json.Caption = (generatedCaption + '\\n\\n' + hashtags.join(' ')).slice(0, 900).trim(); item.json.Note = cleanText(note + ' | AI caption ready gemini=' + (meta.model || 'default')); } else { item.json.Caption = fallback; item.json.Note = cleanText(note + ' | AI caption fallback: empty Gemini caption gemini=' + (meta.model || 'default')); } delete item.json._caption; }\nreturn $input.all();"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4480,
1584
],
"id": "2e49b8dd-0562-4a2c-b145-992404e05b5d",
"name": "Finalize AI Caption"
}
],
"connections": {
"Manual Test Trigger": {
"main": [
[
{
"node": "Read Threads Sheet",
"type": "main",
"index": 0
}
]
]
},
"Phase 3 Start Webhook": {
"main": [
[
{
"node": "Read Threads Sheet",
"type": "main",
"index": 0
}
]
]
},
"Read Threads Sheet": {
"main": [
[
{
"node": "Pick Voice Row",
"type": "main",
"index": 0
},
{
"node": "Read Threads Sheet For Merge",
"type": "main",
"index": 0
}
]
]
},
"Pick Voice Row": {
"main": [
[
{
"node": "Run Voice Generator",
"type": "main",
"index": 0
}
]
]
},
"Pick Visual Row": {
"main": [
[
{
"node": "Run Visual Builder",
"type": "main",
"index": 0
}
]
]
},
"Pick Merge Row": {
"main": [
[
{
"node": "Run Merge Final",
"type": "main",
"index": 0
}
]
]
},
"Run Voice Generator": {
"main": [
[
{
"node": "Update Voice Row",
"type": "main",
"index": 0
}
]
]
},
"Run Visual Builder": {
"main": [
[
{
"node": "Update Visual Row",
"type": "main",
"index": 0
}
]
]
},
"Run Merge Final": {
"main": [
[
{
"node": "Prepare AI Caption",
"type": "main",
"index": 0
}
]
]
},
"Update Voice Row": {
"main": [
[
{
"node": "Read Threads Sheet For Merge",
"type": "main",
"index": 0
}
]
]
},
"Update Visual Row": {
"main": [
[
{
"node": "Read Threads Sheet For Merge After Visual",
"type": "main",
"index": 0
}
]
]
},
"Update Merge Row": {
"main": [
[
{
"node": "Trigger Phase 4",
"type": "main",
"index": 0
}
]
]
},
"Read Threads Sheet For Merge": {
"main": [
[
{
"node": "Pick Visual Row",
"type": "main",
"index": 0
},
{
"node": "Pick Merge Row",
"type": "main",
"index": 0
}
]
]
},
"Read Threads Sheet For Merge After Visual": {
"main": [
[
{
"node": "Pick Merge Row",
"type": "main",
"index": 0
}
]
]
},
"Prepare AI Caption": {
"main": [
[
{
"node": "Gemini AI Caption Request",
"type": "main",
"index": 0
},
{
"node": "Merge AI Caption Result",
"type": "main",
"index": 1
}
]
]
},
"Gemini AI Caption Request": {
"main": [
[
{
"node": "Merge AI Caption Result",
"type": "main",
"index": 0
}
]
]
},
"Merge AI Caption Result": {
"main": [
[
{
"node": "Finalize AI Caption",
"type": "main",
"index": 0
}
]
]
},
"Finalize AI Caption": {
"main": [
[
{
"node": "Update Merge Row",
"type": "main",
"index": 0
}
]
]
}
},
"meta": {
"templateCredsSetupCompleted": true
}
}
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.
googleApigooglePalmApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
03-Video-Maker. Uses googleSheets, httpRequest, googleGemini. Event-driven trigger; 19 nodes.
Source: https://github.com/HoaNgo18/n8n-ai-video/blob/f6873d74ce801e53cf8e662513c87704a0525917/workflows/03-video-maker.json — 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.
Sales Lead Qualifier. Uses telegramTrigger, googleSheets, telegram, googleGemini. Event-driven trigger; 41 nodes.
This workflow reads a gold/silver/oil portfolio from Google Sheets, pulls current commodity prices and a recent gold trend from GoldAPI, EIA, and Twelve Data, calculates allocation and HHI concentrati
Monitor a Google Drive folder, process each image based on the prompt defined in and save the new image to the specified output Google Drive folder. Maintain a processing log in Google Sheets.
Understand your customers before you build for them. This workflow, Market Segmentation: Buyer Persona Pain Point Report, automates the grueling process of primary market research. By scraping real-wo
Categories Marketing Intelligence, Ad Operations, Competitive Research, Creative Analysis