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": "Zero-Cost Social Pipeline (M5)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "social-pipeline",
"responseMode": "onReceived",
"responseData": "noData",
"options": {}
},
"id": "node-webhook-01",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"notes": "Receives POST { videoPath, prompt, context?, platform? }. platform defaults to telegram. Responds immediately."
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\nconst body = item.body && typeof item.body === 'object' ? item.body : item;\nconst videoPath = body.videoPath || item.videoPath;\nconst prompt = body.prompt || item.prompt;\nif (!videoPath || typeof videoPath !== 'string') {\n throw new Error('Missing or empty videoPath in webhook payload');\n}\nif (!prompt || typeof prompt !== 'string') {\n throw new Error('Missing or empty prompt in webhook payload');\n}\nconst denoiseRaw = body.denoise ?? item.denoise ?? 'off';\nconst denoise = ['off', 'light', 'strong'].indexOf(denoiseRaw) !== -1 ? denoiseRaw : 'off';\nconst subtitleYRaw = body.subtitleY ?? item.subtitleY ?? 0.70;\nconst subtitleY = typeof subtitleYRaw === 'number' && subtitleYRaw >= 0 && subtitleYRaw <= 1 ? subtitleYRaw : 0.70;\nconst subtitleSizeRaw = body.subtitleSize ?? item.subtitleSize ?? 'medium';\nconst subtitleSize = ['small', 'medium', 'large'].indexOf(subtitleSizeRaw) !== -1 ? subtitleSizeRaw : 'medium';\nconst caption = body.caption || '';\nconst hashtags = Array.isArray(body.hashtags) ? body.hashtags : [];\nreturn [{\n json: {\n ...item,\n id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),\n videoPath,\n prompt,\n context: body.context ?? item.context ?? null,\n platform: body.platform || item.platform || 'telegram',\n denoise,\n subtitleY,\n subtitleSize,\n caption,\n hashtags\n }\n}];"
},
"id": "node-preflight-02",
"name": "Preflight",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
400,
0
],
"notes": "Validates videoPath and prompt, defaults platform to telegram, generates the run id used in filenames, passes the payload through."
},
{
"parameters": {
"command": "={{ 'D:/Projects/social-media-automation/.venv/Scripts/python.exe D:/Projects/social-media-automation/scripts/video_pipeline.py --transcribe-only --input \"' + $json.videoPath + '\" --transcript-output \"' + 'D:/Projects/social-media-automation/output/transcript_' + $json.id + '.json\"' }}"
},
"id": "node-transcribe-12",
"name": "Transcribe",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
800,
0
],
"notes": "Runs faster-whisper tiny/int8 via video_pipeline --transcribe-only. Transcript text is printed between TRANSCRIPT_TEXT_BEGIN/END markers on stdout."
},
{
"parameters": {
"jsCode": "const exec = $input.first().json;\nconst stdout = exec.stdout || '';\nconst pre = $('Preflight').first().json;\nlet transcript = '';\nconst m = stdout.match(/TRANSCRIPT_TEXT_BEGIN\\n([\\s\\S]*?)TRANSCRIPT_TEXT_END/);\nif (m) transcript = m[1].trim();\nconst isAuto = !pre.prompt || pre.prompt.trim() === '' || pre.prompt.trim().toLowerCase() === 'auto';\nlet prompt = pre.prompt;\nlet context = pre.context;\nif (isAuto) {\n prompt = transcript\n ? 'Write an engaging short-form social media post based on the spoken content of this video clip. Use the actual speech as the source material.'\n : 'Write an engaging short-form social media post about this video clip. Make it energetic and scroll-stopping.';\n context = transcript || null;\n}\nreturn [{\n json: {\n id: pre.id,\n videoPath: pre.videoPath,\n prompt,\n context,\n platform: pre.platform,\n denoise: pre.denoise,\n subtitleY: pre.subtitleY,\n subtitleSize: pre.subtitleSize,\n transcriptPath: 'D:/Projects/social-media-automation/output/transcript_' + pre.id + '.json'\n }\n}];"
},
"id": "node-prepare-gemini-13",
"name": "PrepareGemini",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1200,
0
],
"notes": "When the prompt is empty/'auto', switches to a subtitle-based prompt and feeds the whisper transcript as context so Gemini writes caption/description/tags from the actual speech."
},
{
"parameters": {
"method": "POST",
"url": "={{ 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash-lite:generateContent?key=' + $env.GEMINI_API_KEY }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ systemInstruction: { parts: [{ text: 'You are an expert social media content strategist. You write captions that stop the scroll. Respond with a single JSON object and nothing else. Use exactly this shape: {\"caption\": \"...\", \"description\": \"...\", \"hashtags\": [\"...\"], \"hook\": \"...\"}. Rules: caption is the main post caption, at most 220 characters. description is a longer description or alt text for the video, at most 1000 characters. hashtags is an array of 2 to 8 hashtags as plain words, omit the # symbol, all lowercase, each at most 25 characters, no spaces. hook is a short opening line that grabs attention, at most 60 characters. Write engaging, human, energetic copy. Do not wrap the JSON in markdown fences.' }] }, contents: [{ parts: [{ text: 'Write a social media post about: ' + $json.prompt + ($json.context ? '\\nBackground context:\\n' + $json.context : '') }] }], generationConfig: { responseMimeType: 'application/json', temperature: 0.8, maxOutputTokens: 1024 } }) }}"
},
"id": "node-gemini-03",
"name": "Gemini",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
1600,
0
],
"notes": "Calls Gemini free tier generateContent. Requires GEMINI_API_KEY to be available to n8n as an environment variable."
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\nconst candidates = item.candidates || [];\nconst parts = (candidates[0] && candidates[0].content && candidates[0].content.parts) || [];\nconst text = parts.map(function (p) { return p.text ? p.text : ''; }).join('').trim();\nconst cleaned = text.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '').trim();\nlet parsed;\ntry {\n parsed = JSON.parse(cleaned);\n} catch (e) {\n throw new Error('Gemini response was not valid JSON: ' + text);\n}\nconst required = ['caption', 'description', 'hashtags', 'hook'];\nconst missing = required.filter(function (k) { return parsed[k] === undefined || parsed[k] === null || parsed[k] === ''; });\nif (missing.length > 0) {\n throw new Error('Gemini response is missing required field(s): ' + missing.join(', '));\n}\nconst meta = $('PrepareGemini').first().json;\nreturn [{\n json: {\n ...meta,\n caption: parsed.caption,\n description: parsed.description,\n hashtags: parsed.hashtags,\n hook: parsed.hook\n }\n}];"
},
"id": "node-parse-gemini-04",
"name": "ParseGemini",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2000,
0
],
"notes": "Extracts the JSON object from the Gemini response parts, validates the four required fields, and passes through the run metadata from PrepareGemini."
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\nconst pre = $('Preflight').first().json;\nconst meta = {\n caption: pre.caption || item.caption || '',\n description: pre.description || item.description || '',\n hashtags: pre.hashtags && pre.hashtags.length ? pre.hashtags : (item.hashtags || []),\n hook: pre.hook || item.hook || '',\n platform: item.platform || 'telegram'\n};\nconst metaB64 = Buffer.from(JSON.stringify(meta), 'utf8').toString('base64');\nreturn [{ json: { ...item, meta, metaB64 } }];"
},
"id": "node-encode-meta-14",
"name": "EncodeMeta",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2400,
0
],
"notes": "Packs caption/description/hashtags/hook/platform into base64 so it survives the command line on its way to disk."
},
{
"parameters": {
"command": "={{ 'D:/Projects/social-media-automation/.venv/Scripts/python.exe D:/Projects/social-media-automation/scripts/store_meta.py \"' + $json.id + '\" \"' + $json.metaB64 + '\"' }}"
},
"id": "node-store-meta-15",
"name": "StoreMeta",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
2800,
0
],
"notes": "Writes output/meta_<id>.json so the bot can publish after the user approves the preview."
},
{
"parameters": {
"command": "={{ 'D:/Projects/social-media-automation/.venv/Scripts/python.exe D:/Projects/social-media-automation/scripts/video_pipeline.py --input \"' + $('ParseGemini').first().json.videoPath + '\" --no-caption --output \"' + 'D:/Projects/social-media-automation/output/final_' + $('ParseGemini').first().json.id + '.mp4\" --denoise \"' + ($('ParseGemini').first().json.denoise || 'off') + '\" --subtitles --transcript \"' + $('ParseGemini').first().json.transcriptPath + '\" --subtitle-y \"' + $('ParseGemini').first().json.subtitleY + '\" --subtitle-size \"' + $('ParseGemini').first().json.subtitleSize + '\"' }}"
},
"id": "node-run-video-05",
"name": "RunVideoPipeline",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
3200,
0
],
"notes": "Runs the MoviePy/FFmpeg pipeline: 9:16 crop, denoise, and burned-in subtitles reused from the transcript. Caption text is NOT burned on the video (--no-caption); caption/description/tags are published as post text."
},
{
"parameters": {
"jsCode": "const cmd = $input.first().json;\nconst stdout = cmd.stdout || '';\nconst match = stdout.match(/OUTPUT_VIDEO=(\\S+)/);\nconst outputFile = match ? match[1] : '';\nconst meta = $('ParseGemini').first().json;\nreturn [{\n json: {\n ...meta,\n exitCode: cmd.exitCode,\n stderr: cmd.stderr,\n stdout: cmd.stdout,\n outputFile,\n fileExists: outputFile.length > 0 && cmd.exitCode === 0,\n outputSize: outputFile.length > 0 ? 1 : 0\n }\n}];\n"
},
"id": "node-check-output-06",
"name": "CheckOutputFile",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3600,
0
],
"notes": "Verifies the generated mp4 exists on disk and is non-empty."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond-output-exists-01",
"leftValue": "={{ $json.fileExists }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
}
},
"id": "node-output-exists-07",
"name": "OutputExists",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
4000,
0
],
"notes": "true -> ReadBinaryFile/SendPreview; false -> RaiseError (feeds the configured error workflow)."
},
{
"parameters": {
"jsCode": "const fs = require('fs');\nconst filePath = 'D:/Projects/social-media-automation/output/final_' + $json.id + '.mp4';\nconst stat = fs.statSync(filePath);\nconst buffer = fs.readFileSync(filePath);\nconst binaryData = await this.helpers.prepareBinaryData(buffer, 'video.mp4', 'video/mp4');\nreturn [{ json: $json, binary: { data: binaryData } }];"
},
"id": "node-read-binary-16",
"name": "ReadBinaryFile",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4400,
0
],
"notes": "Loads the rendered mp4 into n8n binary data (property 'data') for the Telegram upload. Uses raw fs.readFileSync to bypass n8n's readBinaryFile path restriction."
},
{
"parameters": {
"method": "POST",
"url": "={{ 'https://api.telegram.org/bot' + $env.TELEGRAM_BOT_TOKEN + '/sendVideo' }}",
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ Number($env.TELEGRAM_CHAT_ID) || $env.TELEGRAM_CHAT_ID }}",
"parameterType": "formData"
},
{
"name": "caption",
"value": "={{ ($json.caption || '') + ($json.hashtags && $json.hashtags.length ? '\\n\\n' + $json.hashtags.map(function (h) { return '#' + h; }).join(' ') : '') + '\\n\\n\ud83d\udc40 Preview for ' + $json.platform + ' \u2014 approve to publish?' }}",
"parameterType": "formData"
},
{
"name": "reply_markup",
"value": "={{ JSON.stringify({ inline_keyboard: [[{ text: '\u2705 Publish', callback_data: 'pv_yes_' + $json.id }, { text: '\u274c Discard', callback_data: 'pv_no_' + $json.id }]] }) }}",
"parameterType": "formData"
},
{
"name": "video",
"value": "",
"parameterType": "formBinaryData",
"inputDataFieldName": "data"
}
]
},
"options": {}
},
"id": "node-send-preview-17",
"name": "SendPreview",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
4800,
0
],
"notes": "Sends the full rendered video to the user's Telegram chat with \u2705 Publish / \u274c Discard buttons. Nothing is posted yet; the bot publishes on approval."
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\nthrow new Error('Video pipeline failed: output file missing or empty (' + item.outputFile + '). exitCode=' + item.exitCode + ' stderr=' + (item.stderr || ''));"
},
"id": "node-raise-error-10",
"name": "RaiseError",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4400,
200
],
"notes": "Throws so the execution fails and the configured error workflow (social_pipeline_error) fires."
},
{
"parameters": {},
"id": "node-end-11",
"name": "End",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
5200,
0
]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Preflight",
"type": "main",
"index": 0
}
]
]
},
"Preflight": {
"main": [
[
{
"node": "Transcribe",
"type": "main",
"index": 0
}
]
]
},
"Transcribe": {
"main": [
[
{
"node": "PrepareGemini",
"type": "main",
"index": 0
}
]
]
},
"PrepareGemini": {
"main": [
[
{
"node": "Gemini",
"type": "main",
"index": 0
}
]
]
},
"Gemini": {
"main": [
[
{
"node": "ParseGemini",
"type": "main",
"index": 0
}
]
]
},
"ParseGemini": {
"main": [
[
{
"node": "EncodeMeta",
"type": "main",
"index": 0
}
]
]
},
"EncodeMeta": {
"main": [
[
{
"node": "StoreMeta",
"type": "main",
"index": 0
}
]
]
},
"StoreMeta": {
"main": [
[
{
"node": "RunVideoPipeline",
"type": "main",
"index": 0
}
]
]
},
"RunVideoPipeline": {
"main": [
[
{
"node": "CheckOutputFile",
"type": "main",
"index": 0
}
]
]
},
"CheckOutputFile": {
"main": [
[
{
"node": "OutputExists",
"type": "main",
"index": 0
}
]
]
},
"OutputExists": {
"main": [
[
{
"node": "ReadBinaryFile",
"type": "main",
"index": 0
}
],
[
{
"node": "RaiseError",
"type": "main",
"index": 0
}
]
]
},
"ReadBinaryFile": {
"main": [
[
{
"node": "SendPreview",
"type": "main",
"index": 0
}
]
]
},
"SendPreview": {
"main": [
[
{
"node": "End",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"errorWorkflow": "2"
},
"active": true,
"versionId": "2c4d08a3-4b54-43ec-847a-44f545e7c6a1",
"tags": [],
"id": 1
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Zero-Cost Social Pipeline (M5). Uses executeCommand, httpRequest. Webhook trigger; 15 nodes.
Source: https://github.com/tamimlabs/social-media-automation/blob/main/workflows/social_pipeline.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.
Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.
MLOps Pipeline EN-PT. Uses executeCommand, httpRequest, errorTrigger. Webhook trigger; 18 nodes.
AI Product Video Generator (Windows). Uses httpRequest, writeBinaryFile, executeCommand, readBinaryFile. Webhook trigger; 16 nodes.
MLOps Pipeline - Hand Talk. Uses executeCommand, httpRequest. Webhook trigger; 15 nodes.
AIDP - Main Workflow v2. Uses executeCommand, httpRequest. Webhook trigger; 13 nodes.