This workflow follows the Executecommand → 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 →
{
"name": "AI News Video - COMPLETE WITH GOOGLE TTS (JWT token flow)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "ai-news-google-tts-complete",
"responseMode": "lastNode",
"options": {}
},
"id": "webhook-trigger",
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [
250,
480
]
},
{
"parameters": {
"jsCode": "// Generate 3 AI news items\nconst articles = [\n { title: 'OpenAI Unveils GPT-5 with Revolutionary Reasoning Capabilities', snippet: 'OpenAI announces GPT-5 featuring breakthrough reasoning abilities and unprecedented accuracy', index: 0 },\n { title: 'Google Achieves Major Quantum AI Computing Breakthrough', snippet: 'Google reveals groundbreaking advancement in quantum computing integrated with AI', index: 1 },\n { title: 'Microsoft Copilot Receives Enterprise-Grade AI Upgrade', snippet: 'Microsoft enhances Copilot with advanced AI capabilities and enterprise security', index: 2 }\n];\nreturn articles.map(a => ({ json: a }));"
},
"id": "generate-news",
"name": "Generate News",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
450,
480
]
},
{
"parameters": {
"jsCode": "// Generate script\nconst title = String($json.title || 'AI News');\nconst snippet = String($json.snippet || '');\nconst index = Number($json.index || 0);\nconst templates = [\n `Breaking AI news: ${title}. ${snippet}. This marks a major milestone in artificial intelligence.`,\n `Major update: ${title}. ${snippet}. Industry experts call this a game-changing advancement.`,\n `Latest AI: ${title}. ${snippet}. This innovation promises to revolutionize technology.`\n];\nlet script = templates[index % templates.length];\nif (script.length > 180) script = script.substring(0, 177) + '...';\nreturn { json: { title, snippet, script, index } };\n"
},
"id": "generate-script",
"name": "Generate Script",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
650,
480
]
},
{
"parameters": {
"jsCode": "// Create JWT and request OAuth access token using service account JSON\n// NOTE: this node contains your service account json. If you want it to be stored elsewhere, replace SA_JSON.\nconst SA_JSON = {\n \"type\": \"service_account\",\n \"project_id\": \"lateral-goods-477606-i1\",\n \"private_key_id\": \"5c6d4ca06ed90142ec2a6e8f37d2b59e6d823277\",\n \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCi7LsMHfCAow3r\\nHt6soc0ktc2HNKBXeNmbb9TdxJ4lKo3vkdqnA5kcC+Cext9g6JL67iJLFCtm7b7U\\nzSzTCld0KBWVymQk1wjNI//qgBHRHVc9MIorwA+XAhVsU2olhC6YXwUrc3+GY4ai\\nUR2AlJq+ooXqTAcikMQWhpZVpl8zOUoUxGsZR1m6R4Wxo8wgHX3f7p/SPnq3sg8S\\nVKX3G31qVCuwwlJO4FyYWSzIMDpqDT/0M8xoFXQa99jbdTX/QDQCBpcGnGHWArvS\\nh92Dz1rshlojl3P0pK42tBJ4Qp0WDOLEqJesOAXs32jfeeXSLfCMIGlwUYnEyc/v\\nBxcpcY57AgMBAAECggEAAzTsqOuYLVuDHNwASTMA9RAEHdQwyDYMPfC5te8+NpIt\\nnDNxHxfH1kKaETQMtLLzqfbzX3HPdvY1Dt0dhbQY9x63+ZA49Imh2MulPOQwvUOo\\nLTLc0iViuIchprwD+Jck9jJ6gX2NwX4dl/Oj97vqzoXBMNnF6i2xLlij9xkLDGf7\\n4GZbT4gx/ZzpzwalUTjt1QMYChgohBZbbMK7SaXXhWvd5A3UCvvxBhxKyKYAlTDO\\nPenxS1b8hLvkzc8ECbzmIPQIp1YJrzz+QCe2Vkzmdi5nmQq8nD1WCoLEn7bCWNoZ\\nrwUIhlk9MOaiOiW+AYR9aub32LNHypgXRph/6u9GgQKBgQDc3MmF004PuNaED9UV\\nCQah4u6JG4w5ioqLjujQ793SbLdaC/Iaxu0JuX9Y7zgU6zXJKz1zYOf/T5voqbar\\nYkkko9kq/lsQ5l422gTCkrdAGmj0fLDfjr1FG6BkSfmeOQmoZ5Uf6GVAjQP5I6c9\\nr1e51o12pxmxAZLAG1fQja89MwKBgQC82EZunX2HFzAnFPvleiDkBJCE4tvHWfXi\\nSIjIODw6skFIKkRqv2VZtAH6PeryT0C2vMuzXhBl1IypPFqogLVrX1YAixY+7rLM\\niddcn0nnBZtECgSDY/5ULbbwsGBWpRGV6Pv/MOWxQyeYu1ImaNXKO0bUHTCi4px+\\nZo8ujlQZmQKBgQCNMrG7Vq2vK3IpF54YRp7w3A23pd7t4n5UXlbFTLQ5lLtbXAu5\\nxrc/4lFh3/2wkfbe10AABVIMTS7VfbqEst8kB4QNEnPRyBUvaA5m/jkdSEUVGKpT\\nIgQqrFDMDOcCmmBsQ1x4+6/PpteFbZ+7td+VtW7XDllEakcRfemUMSB5NQKBgQCZ\\n3oLs6Ef6hYtHnNJuNSeNgqaakBnRgdxWBxHkSeXRUaLdgQsEC3UyNPiThFXmH2s0\\nOfqj6JXl0tzVnAamW1D27tQtVybGGkn3XKzsnCFkKm5LbvokcJouzpzL2np0vsTo\\nZ9DEKnxNBdHCoYabIzpnMAtTE4GohopKd5hcr72YqQKBgCudbVkwO57mXQ0ligDp\\nP9rOLTuH4YVXNcj3yVLxLdp7wALzGwUVShjwigWoXzUWef2pxDfwWAPDhQ1dt0sq\\n+qcyzQdw/rGkQ+FHeoq+GrLr19u8ef/uWFgtcoDksbntQvVkM2JwrSmefWSZ4Atm\\nVvZ/xk5KOreIjUfFmMuuUAxL\\n-----END PRIVATE KEY-----\\n\",\n \"client_email\": \"vertex-ai-n8n@lateral-goods-477606-i1.iam.gserviceaccount.com\",\n \"client_id\": \"104790218756773876337\",\n \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n \"token_uri\": \"https://oauth2.googleapis.com/token\",\n \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/vertex-ai-n8n%40lateral-goods-477606-i1.iam.gserviceaccount.com\",\n \"universe_domain\": \"googleapis.com\"\n};\n\nconst crypto = require('crypto');\nconst https = require('https');\n\nfunction base64url(input) {\n return input.toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nasync function postJson(url, body, headers = {}) {\n return new Promise((resolve, reject) => {\n const u = new URL(url);\n const data = JSON.stringify(body);\n const opts = {\n method: 'POST',\n hostname: u.hostname,\n path: u.pathname + (u.search || ''),\n headers: Object.assign({ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, headers)\n };\n const req = https.request(opts, (res) => {\n let out = '';\n res.on('data', (d) => out += d);\n res.on('end', () => {\n try { resolve(JSON.parse(out)); } catch (e) { reject(new Error('Invalid JSON response: ' + out)); }\n });\n });\n req.on('error', reject);\n req.write(data);\n req.end();\n });\n}\n\n(async () => {\n try {\n const iat = Math.floor(Date.now() / 1000);\n const exp = iat + 3600;\n const header = { alg: 'RS256', typ: 'JWT' };\n const scope = 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/cloud-platform.read-only';\n const payload = {\n iss: SA_JSON.client_email,\n sub: SA_JSON.client_email,\n scope: scope,\n aud: SA_JSON.token_uri,\n iat: iat,\n exp: exp\n };\n const signingInput = base64url(Buffer.from(JSON.stringify(header))) + '.' + base64url(Buffer.from(JSON.stringify(payload)));\n const signer = crypto.createSign('RSA-SHA256');\n signer.update(signingInput);\n signer.end();\n const signature = signer.sign(SA_JSON.private_key);\n const jwt = signingInput + '.' + base64url(signature);\n\n // Exchange JWT for access token\n const tokenResp = await postJson(SA_JSON.token_uri, {\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt\n }, { 'Content-Type': 'application/x-www-form-urlencoded' });\n\n if (!tokenResp.access_token) throw new Error('Token response missing access_token: ' + JSON.stringify(tokenResp));\n\n return [{ json: { access_token: tokenResp.access_token, expires_in: tokenResp.expires_in, token_type: tokenResp.token_type } }];\n } catch (err) {\n throw new Error('Failed to obtain access token: ' + err.message);\n }\n})();\n"
},
"id": "get-access-token",
"name": "Create JWT & Get Access Token",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
850,
480
]
},
{
"parameters": {
"mode": "combine",
"combinationMode": "multiplex",
"options": {}
},
"id": "merge-data",
"name": "Merge Script + Token",
"type": "n8n-nodes-base.merge",
"typeVersion": 2.1,
"position": [
1050,
480
]
},
{
"parameters": {
"method": "POST",
"url": "https://texttospeech.googleapis.com/v1/text:synthesize",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $node['Create JWT & Get Access Token'].json.access_token }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"input\": { \"text\": {{ JSON.stringify($json.script) }} },\n \"voice\": { \"languageCode\": \"en-US\", \"name\": \"en-US-Neural2-J\", \"ssmlGender\": \"MALE\" },\n \"audioConfig\": { \"audioEncoding\": \"MP3\", \"speakingRate\": 1.0, \"pitch\": 0.0, \"volumeGainDb\": 0.0, \"sampleRateHertz\": 24000 }\n}",
"options": {}
},
"id": "gemini-tts-call",
"name": "Google Cloud TTS API Call",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1250,
480
]
},
{
"parameters": {
"jsCode": "// Convert base64 audio from Google TTS API response to binary\nconst title = String($input.item.json.title || 'AI News');\nconst snippet = String($input.item.json.snippet || '');\nconst script = String($input.item.json.script || '');\nconst index = Number($input.item.json.index || 0);\n\nconst audioContent = $input.item.json.audioContent || $input.item.json.audioContent;\nif (!audioContent) {\n throw new Error('No audio content received from Google TTS API');\n}\n\nconst audioBuffer = Buffer.from(audioContent, 'base64');\nreturn {\n json: { title, snippet, script, index, voiceFile: `voice_${index}.mp3` },\n binary: { audio: { data: audioBuffer.toString('base64'), mimeType: 'audio/mpeg', fileName: `voice_${index}.mp3`, fileExtension: 'mp3' } }\n};"
},
"id": "prepare-audio-data",
"name": "Prepare Audio Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1450,
480
]
},
{
"parameters": {
"operation": "write",
"fileName": "=voice_{{ $json.index }}.mp3",
"dataPropertyName": "audio",
"options": {
"folderPath": "d:/AI-AGENT/output"
}
},
"id": "save-voice",
"name": "Save Voice File",
"type": "n8n-nodes-base.writeBinaryFile",
"typeVersion": 1,
"position": [
1650,
480
]
},
{
"parameters": {
"jsCode": "// Generate subtitles\nconst script = String($input.item.json.script || '');\nconst index = Number($input.item.json.index || 0);\nconst words = script.split(/\\s+/);\nconst chunks = [];\nlet currentChunk = [];\nlet chunkLength = 0;\nfor (const word of words) {\n currentChunk.push(word);\n chunkLength += word.length + 1;\n if (chunkLength > 55 || /[.!?,]$/.test(word)) { chunks.push(currentChunk.join(' ')); currentChunk = []; chunkLength = 0; }\n}\nif (currentChunk.length > 0) chunks.push(currentChunk.join(' '));\nif (chunks.length === 0) chunks.push(script);\nlet srtContent = '';\nlet startTime = 0;\nchunks.forEach((chunk, i) => {\n const duration = Math.max(2, Math.ceil(chunk.length / 12));\n const endTime = startTime + duration;\n const formatTime = (sec) => {\n const h = String(Math.floor(sec / 3600)).padStart(2, '0');\n const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0');\n const s = String(Math.floor(sec % 60)).padStart(2, '0');\n return `${h}:${m}:${s},000`;\n };\n srtContent += `${i + 1}\\n${formatTime(startTime)} --> ${formatTime(endTime)}\\n${chunk.trim()}\\n\\n`;\n startTime = endTime;\n});\nconst buffer = Buffer.from(srtContent, 'utf-8');\nreturn { json: { title: $input.item.json.title, script: script, index: index, voiceFile: `voice_${index}.mp3`, subtitleFile: `subtitles_${index}.srt`, duration: startTime }, binary: { data: { data: buffer.toString('base64'), mimeType: 'text/plain', fileName: `subtitles_${index}.srt`, fileExtension: 'srt' } } };\n"
},
"id": "generate-subtitles",
"name": "Generate Subtitles",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1850,
480
]
},
{
"parameters": {
"operation": "write",
"fileName": "={{ $json.subtitleFile }}",
"dataPropertyName": "data",
"options": {
"folderPath": "d:/AI-AGENT/output"
}
},
"id": "save-subtitles",
"name": "Save Subtitles",
"type": "n8n-nodes-base.writeBinaryFile",
"typeVersion": 1,
"position": [
2050,
480
]
},
{
"parameters": {
"command": "=cd d:\\AI-AGENT\\output && ffmpeg -y -loop 1 -i \"d:\\AI-AGENT\\ai_background.jpg\" -i \"{{ $json.voiceFile }}\" -vf \"scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,subtitles={{ $json.subtitleFile }}:force_style='FontName=Arial Bold,FontSize=28,PrimaryColour=&HFFFFFF,OutlineColour=&H000000,BorderStyle=3,Outline=3,Shadow=2,MarginV=60,Alignment=2'\" -c:v libx264 -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -shortest -t {{ $json.duration }} \"video_{{ $json.index }}.mp4\"",
"options": {}
},
"id": "create-video",
"name": "Create Video",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
2050,
480
]
},
{
"parameters": {
"jsCode": "// Aggregate results\nconst allItems = $input.all();\nconst videos = [];\nfor (const item of allItems) {\n const index = item.json.index || 0;\n videos.push({ index: index, title: item.json.title || 'Untitled', videoFile: `video_${index}.mp4`, voiceFile: `voice_${index}.mp3`, subtitleFile: `subtitles_${index}.srt`, duration: item.json.duration || 20 });\n}\nvideos.sort((a,b)=>a.index-b.index);\nconst fileListContent = videos.map(v => `file 'video_${v.index}.mp4'`).join('\\n');\nconst fileListBuffer = Buffer.from(fileListContent,'utf-8');\nconst totalDuration = videos.reduce((sum,v)=>sum+v.duration,0);\nconst summary = `\u2705 SUCCESS: ${videos.length} videos created with Google Cloud TTS!\\n\\nTotal Duration: ${totalDuration}s\\n\\nFiles:\\n${videos.map((v,i)=>`${i+1}. ${v.videoFile}`).join('\\n')}\\n\\nFinal: AI_NEWS_FINAL_COMPLETE.mp4`;\nconst summaryBuffer = Buffer.from(summary,'utf-8');\nreturn [{ json: { success: true, totalVideos: videos.length, totalDuration: totalDuration, videos: videos, outputFolder: 'd:/AI-AGENT/output/' }, binary: { summary: { data: summaryBuffer.toString('base64'), mimeType: 'text/plain', fileName: 'SUMMARY.txt', fileExtension: 'txt' }, filelist: { data: fileListBuffer.toString('base64'), mimeType: 'text/plain', fileName: 'filelist.txt', fileExtension: 'txt' } } }];"
},
"id": "aggregate",
"name": "Aggregate Results",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2250,
480
]
},
{
"parameters": {
"operation": "write",
"fileName": "filelist.txt",
"dataPropertyName": "filelist",
"options": {
"folderPath": "d:/AI-AGENT/output"
}
},
"id": "save-filelist",
"name": "Save Filelist",
"type": "n8n-nodes-base.writeBinaryFile",
"typeVersion": 1,
"position": [
2450,
480
]
},
{
"parameters": {
"operation": "write",
"fileName": "SUMMARY.txt",
"dataPropertyName": "summary",
"options": {
"folderPath": "d:/AI-AGENT/output"
}
},
"id": "save-summary",
"name": "Save Summary",
"type": "n8n-nodes-base.writeBinaryFile",
"typeVersion": 1,
"position": [
2650,
480
]
},
{
"parameters": {
"command": "cd d:\\AI-AGENT\\output && ffmpeg -y -f concat -safe 0 -i filelist.txt -c copy AI_NEWS_FINAL_COMPLETE.mp4",
"options": {}
},
"id": "final-video",
"name": "Create Final Video",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
2850,
480
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { success: true, message: '\ud83c\udf89 Video Ready with Google Cloud TTS!', finalVideo: 'd:/AI-AGENT/output/AI_NEWS_FINAL_COMPLETE.mp4', videos: $json.videos, totalDuration: $json.totalDuration } }}"
},
"id": "response",
"name": "Send Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
3050,
480
]
}
],
"connections": {
"Webhook Trigger": {
"main": [
[
{
"node": "Generate News",
"type": "main",
"index": 0
}
]
]
},
"Generate News": {
"main": [
[
{
"node": "Generate Script",
"type": "main",
"index": 0
}
]
]
},
"Generate Script": {
"main": [
[
{
"node": "Create JWT & Get Access Token",
"type": "main",
"index": 0
},
{
"node": "Merge Script + Token",
"type": "main",
"index": 0
}
]
]
},
"Create JWT & Get Access Token": {
"main": [
[
{
"node": "Merge Script + Token",
"type": "main",
"index": 1
}
]
]
},
"Merge Script + Token": {
"main": [
[
{
"node": "Google Cloud TTS API Call",
"type": "main",
"index": 0
}
]
]
},
"Google Cloud TTS API Call": {
"main": [
[
{
"node": "Prepare Audio Data",
"type": "main",
"index": 0
}
]
]
},
"Prepare Audio Data": {
"main": [
[
{
"node": "Save Voice File",
"type": "main",
"index": 0
}
]
]
},
"Save Voice File": {
"main": [
[
{
"node": "Generate Subtitles",
"type": "main",
"index": 0
}
]
]
},
"Generate Subtitles": {
"main": [
[
{
"node": "Save Subtitles",
"type": "main",
"index": 0
}
]
]
},
"Save Subtitles": {
"main": [
[
{
"node": "Create Video",
"type": "main",
"index": 0
}
]
]
},
"Create Video": {
"main": [
[
{
"node": "Aggregate Results",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Results": {
"main": [
[
{
"node": "Save Filelist",
"type": "main",
"index": 0
},
{
"node": "Save Summary",
"type": "main",
"index": 0
}
]
]
},
"Save Filelist": {
"main": [
[
{
"node": "Create Final Video",
"type": "main",
"index": 0
}
]
]
},
"Save Summary": {
"main": [
[
{
"node": "Create Final Video",
"type": "main",
"index": 0
}
]
]
},
"Create Final Video": {
"main": [
[
{
"node": "Send Response",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
AI News Video - COMPLETE WITH GOOGLE TTS (JWT token flow). Uses httpRequest, writeBinaryFile, executeCommand. Webhook trigger; 16 nodes.
Source: https://gist.github.com/Shubhamagarwal100/2a59b72293f09434185a07a30137c2e4 — 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.
Use cases Auto-generate subtitles for training or educational videos Translate videos into multiple languages for global reach Create accessibility-friendly content with minimal effort Build a backend
TestFixer CI Integration. Uses executeCommand, httpRequest, slack. Webhook trigger; 16 nodes.
PRAGMAS - Main Analysis. Uses httpRequest, executeCommand. Webhook trigger; 6 nodes.
MallanooSploit. Uses openAi, executeCommand, httpRequest. Webhook trigger; 44 nodes.
Automatically creates complete videos from a text prompt—script, voiceover, stock footage, and subtitles all assembled and ready.