AutomationFlowsSlack & Telegram › Async Video Polling Engine - Background Job Handler for AI Video Generation

Async Video Polling Engine - Background Job Handler for AI Video Generation

Original n8n title: 🔄 Async Video Polling Engine - Background Job Handler for AI Video Generation

ByJoe V @joevenner on n8n.io

[](https://www.linkedin.com/in/mosaab-yassir-lafrimi/)[](https://t.me/joevenner)

Webhook trigger★★★★★ complexity35 nodesHTTP RequestRedisS3Telegram
Slack & Telegram Trigger: Webhook Nodes: 35 Complexity: ★★★★★ Added:
Async Video Polling Engine - Background Job Handler for AI Video Generation — n8n workflow card showing HTTP Request, Redis, S3 integration

This workflow corresponds to n8n.io template #12684 — we link there as the canonical source.

This workflow follows the HTTP Request → Telegram 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 →

Download .json
{
  "id": "BBGHaZpPUQXWrFaQ",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Poll video generation status from KIE.ai and deliver via Telegram (Sanitized)",
  "tags": [],
  "nodes": [
    {
      "id": "b174f32b-dc84-426e-9f24-9ae3ebab200b",
      "name": "Main",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3232,
        368
      ],
      "parameters": {
        "width": 380,
        "height": 576,
        "content": "## How it works\n\n1. Receives a webhook call with video task ID from the main workflow\n2. Waits 1 minute, then checks KIE.ai API for generation status\n3. If still processing, retries (up to 15 attempts / 15 min)\n4. Once complete, downloads video and uploads to S3\n5. Retrieves session metadata from Redis\n6. Sends video preview to user via Telegram with publish buttons\n\nSupports Veo 3.1, Sora 2, and Seedance models.\n\n## Setup steps\n\n1. Add your KIE.ai API key (Header Auth)\n2. Configure Redis connection for session storage\n3. Set up S3 bucket credentials\n4. Add Telegram Bot API credentials\n5. Update the webhook URL in \"Retry Poll\" node to match your n8n instance"
      },
      "typeVersion": 1
    },
    {
      "id": "6d2ef622-0e8e-4e7f-9ef1-b81603f5087f",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2704,
        448
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 80,
        "content": "**Webhook & Parse** \u2014 Receives polling request, parses input data, responds OK immediately"
      },
      "typeVersion": 1
    },
    {
      "id": "8707a955-08d5-4494-8fac-14f1894968e5",
      "name": "Status Polling",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1968,
        464
      ],
      "parameters": {
        "color": 4,
        "width": 696,
        "height": 352,
        "content": "**Status Polling** \u2014 Waits 1 min, checks KIE.ai status, parses response for completion"
      },
      "typeVersion": 1
    },
    {
      "id": "b45a0f9e-b1e9-4c94-aabf-9e4c384f7d30",
      "name": "Retry Logic",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1184,
        576
      ],
      "parameters": {
        "color": 3,
        "width": 508,
        "height": 384,
        "content": "**Retry Logic** \u2014 Retries up to 15x or sends timeout message"
      },
      "typeVersion": 1
    },
    {
      "id": "3357d5d2-a485-454c-b6dc-02981f3292db",
      "name": "Video Download",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1184,
        320
      ],
      "parameters": {
        "color": 2,
        "width": 920,
        "height": 224,
        "content": "**Video Processing** \u2014 Downloads from KIE.ai, uploads to S3, checks if extend flow"
      },
      "typeVersion": 1
    },
    {
      "id": "6aabf895-fa67-4e3b-8263-7e6ef92d22ad",
      "name": "Session Storage",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 1112,
        "height": 368,
        "content": "**Session & Metadata** \u2014 Retrieves session from Redis, creates metadata, updates status"
      },
      "typeVersion": 1
    },
    {
      "id": "c028e9e5-56aa-4b68-a80b-acbc7465b8fe",
      "name": "Telegram Delivery",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1744,
        144
      ],
      "parameters": {
        "color": 4,
        "width": 488,
        "height": 608,
        "content": "**Telegram Delivery** \u2014 Sends video preview with publish action buttons"
      },
      "typeVersion": 1
    },
    {
      "id": "ed1d90e8-a6c5-4dd7-a2c1-32ac354fa348",
      "name": "Extend Flow",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 340,
        "height": 80,
        "content": "**Extend Flow** \u2014 Add Merge Option if its an extend request, if initial request skip."
      },
      "typeVersion": 1
    },
    {
      "id": "b3b9a267-f069-41ca-a55f-3e3092ae3e6b",
      "name": "Poll Trigger",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -2672,
        576
      ],
      "parameters": {
        "path": "video-poll",
        "options": {
          "rawBody": true
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "adfb0fc8-4a08-45b7-990a-8733b026cf68",
      "name": "Parse Input",
      "type": "n8n-nodes-base.code",
      "position": [
        -2448,
        576
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\n// Handle different webhook data structures:\n// 1. rawBody enabled: body is a JSON string\n// 2. rawBody disabled: body is an object or data is at root level\nlet body;\nif (typeof input.body === 'string') {\n  body = JSON.parse(input.body);\n} else if (input.body && typeof input.body === 'object') {\n  body = input.body;\n} else {\n  body = input;\n}\n\nreturn {\n  json: {\n    sessionId: body.sessionId,\n    taskId: body.taskId,\n    chatId: body.chatId,\n    messageId: body.messageId,\n    selectedModel: body.selectedModel || 'veo3_fast',\n    modelType: body.modelType || 'veo',\n    statusEndpoint: body.statusEndpoint || '/api/v1/veo/record-info',\n    attempt: body.attempt || 1,\n    isExtend: body.isExtend || false,\n    parentSessionId: body.parentSessionId || null\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "d4c934e6-4b24-4c33-b08c-9dddcc77055f",
      "name": "Respond OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "onError": "continueRegularOutput",
      "position": [
        -2224,
        576
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, attempt: $json.attempt }) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "7019d8f9-4cbc-4e88-ad7e-08121dce9801",
      "name": "Wait 1 Minute",
      "type": "n8n-nodes-base.wait",
      "position": [
        -2000,
        576
      ],
      "parameters": {
        "amount": 60
      },
      "typeVersion": 1.1
    },
    {
      "id": "436c017b-1e8c-4c6b-b2d3-07d932b94bf3",
      "name": "Check KIE Status",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        -1776,
        576
      ],
      "parameters": {
        "url": "=https://api.kie.ai{{ $('Parse Input').first().json.statusEndpoint }}",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "taskId",
              "value": "={{ $('Parse Input').first().json.taskId }}"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "ac9fba69-a574-4382-83a6-c01cfbd6f4b4",
      "name": "Parse Status",
      "type": "n8n-nodes-base.code",
      "position": [
        -1552,
        576
      ],
      "parameters": {
        "jsCode": "const inputData = $('Parse Input').first().json;\nconst statusResponse = $input.first().json;\nconst modelType = inputData.modelType || 'veo';\n\nlet isCompleted = false;\nlet isFailed = false;\nlet videoUrl = null;\nlet status = 'processing';\n\nif (modelType === 'veo') {\n  // Veo response: successFlag 0=Generating, 1=Success, 2=Failed, 3=Generation Failed\n  const successFlag = statusResponse.data?.successFlag;\n  isCompleted = successFlag === 1;\n  isFailed = successFlag === 2 || successFlag === 3;\n  // For 9:16 aspect ratio, use originUrls; otherwise use resultUrls\n  const resultUrls = statusResponse.data?.response?.resultUrls || [];\n  const originUrls = statusResponse.data?.response?.originUrls || [];\n  videoUrl = originUrls[0] || resultUrls[0] || null;\n} else if (modelType === 'market') {\n  // Market API response format (Sora2, Seedance)\n  // State can be: 'pending', 'processing', 'success', 'failed'\n  const taskState = statusResponse.data?.state || statusResponse.data?.status;\n  isCompleted = taskState === 'success' || taskState === 'completed' || taskState === 'COMPLETED';\n  isFailed = taskState === 'failed' || taskState === 'FAILED' || taskState === 'error';\n  \n  // Try multiple locations for video URL\n  // 1. Direct fields\n  videoUrl = statusResponse.data?.videoUrl || statusResponse.data?.output?.video || statusResponse.data?.result?.video_url;\n  \n  // 2. Parse resultJson if it exists\n  if (!videoUrl && statusResponse.data?.resultJson) {\n    try {\n      const resultJson = JSON.parse(statusResponse.data.resultJson);\n      videoUrl = resultJson.resultUrls?.[0] || resultJson.videoUrl || resultJson.video_url || resultJson.url;\n    } catch (e) {\n      // resultJson parsing failed, continue with null\n    }\n  }\n  \n  // 3. Check response object\n  if (!videoUrl && statusResponse.data?.response) {\n    const response = typeof statusResponse.data.response === 'string' \n      ? JSON.parse(statusResponse.data.response) \n      : statusResponse.data.response;\n    videoUrl = response.resultUrls?.[0] || response.videoUrl || response.video_url;\n  }\n}\n\nif (isCompleted) status = 'completed';\nif (isFailed) status = 'failed';\n\nreturn {\n  json: {\n    ...inputData,\n    status,\n    isCompleted,\n    isFailed,\n    videoUrl,\n    rawResponse: statusResponse\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "531e9b09-c985-488e-b8ad-1c0cac398d30",
      "name": "Completed?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1328,
        576
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.isCompleted }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "bd98177f-9a95-4f18-abef-f8c1006bac30",
      "name": "Get 1080p Video",
      "type": "n8n-nodes-base.code",
      "position": [
        -1104,
        384
      ],
      "parameters": {
        "jsCode": "// Pass through - video URL already obtained from record-info\nreturn $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "f7539aaa-b9a2-41f3-9dae-5774b2799903",
      "name": "Get Final URL",
      "type": "n8n-nodes-base.code",
      "position": [
        -880,
        384
      ],
      "parameters": {
        "jsCode": "// videoUrl already available from Parse Status via record-info endpoint\nconst inputData = $('Parse Status').first().json;\nconst parseInputData = $('Parse Input').first().json;\n\nreturn {\n  json: {\n    ...inputData,\n    isExtend: parseInputData.isExtend || false,\n    parentSessionId: parseInputData.parentSessionId || null\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "47b24e8c-810c-4c87-b028-d07a1e234608",
      "name": "Download Video",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -656,
        384
      ],
      "parameters": {
        "url": "={{ $json.videoUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "df9664c3-8d88-4b67-a543-f906db0faf8e",
      "name": "Get Session",
      "type": "n8n-nodes-base.redis",
      "position": [
        464,
        384
      ],
      "parameters": {
        "key": "=session:{{ $('Get Final URL').first().json.sessionId }}",
        "options": {},
        "operation": "get"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "4ccd466e-32eb-4465-83f1-433b5664198d",
      "name": "Merge Session Data",
      "type": "n8n-nodes-base.code",
      "position": [
        688,
        384
      ],
      "parameters": {
        "jsCode": "// Get data from Merge After Concat (via Skip Concat or Concatenate path)\nconst mergeData = $('Merge After Concat').first().json;\nconst redisResult = $input.first().json;\n\n// Redis returns data in propertyName field\nconst redisValue = redisResult.propertyName || redisResult;\nconst session = typeof redisValue === 'string' ? JSON.parse(redisValue) : redisValue;\n\nconst s3Url = `https://your-s3-endpoint.com/shorts/videos/${session.sessionId}.mp4`;\n\n// Get video URL from merge data (passed through Skip Concat or Concat path)\nlet videoUrl = mergeData.videoUrl;\n\n// Fallback: try to get from rawResponse\nif (!videoUrl && mergeData.rawResponse) {\n  const raw = mergeData.rawResponse;\n  videoUrl = raw.data?.response?.originUrls?.[0] || raw.data?.response?.resultUrls?.[0];\n}\n\n// Fallback 2: try Get Final URL directly\nif (!videoUrl) {\n  try {\n    const finalUrlData = $('Get Final URL').first().json;\n    videoUrl = finalUrlData.videoUrl;\n    if (!videoUrl && finalUrlData.rawResponse) {\n      videoUrl = finalUrlData.rawResponse.data?.response?.originUrls?.[0] || finalUrlData.rawResponse.data?.response?.resultUrls?.[0];\n    }\n  } catch (e) {\n    // Get Final URL not accessible\n  }\n}\n\n// Store video URL and raw response for publishing\nreturn {\n  json: {\n    ...session,\n    videoUrl: videoUrl,\n    rawResponse: mergeData.rawResponse,\n    s3Url: s3Url,\n    status: 'video_ready',\n    updatedAt: new Date().toISOString()\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "e2c2c634-d4ad-4be6-84cd-be68f136237c",
      "name": "Update Session",
      "type": "n8n-nodes-base.redis",
      "position": [
        1360,
        384
      ],
      "parameters": {
        "key": "=session:{{ $json.sessionId }}",
        "ttl": 86400,
        "value": "={{ JSON.stringify($json) }}",
        "expire": true,
        "operation": "set"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2d0606a9-2ab0-4ffd-a32f-89dca688dce6",
      "name": "Download for Telegram",
      "type": "n8n-nodes-base.s3",
      "position": [
        1584,
        384
      ],
      "parameters": {
        "fileKey": "=videos/{{ $('Create Metadata').first().json.sessionId }}.mp4",
        "bucketName": "shorts"
      },
      "credentials": {
        "s3": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "bba58015-be7e-4a45-9749-b9a52e864e20",
      "name": "Is Veo Model?",
      "type": "n8n-nodes-base.if",
      "position": [
        1808,
        384
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $('Create Metadata').first().json.selectedModel?.startsWith('veo') }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d7161b85-5598-4711-b0c1-efecfb6a1bff",
      "name": "Send Video Preview (Veo)",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2032,
        288
      ],
      "parameters": {
        "chatId": "={{ $('Create Metadata').first().json.chatId }}",
        "operation": "sendVideo",
        "binaryData": true,
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "YouTube",
                    "additionalFields": {
                      "callback_data": "=publish:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  },
                  {
                    "text": "TikTok",
                    "additionalFields": {
                      "callback_data": "=tiktok:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "Instagram",
                    "additionalFields": {
                      "callback_data": "=instagram:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  },
                  {
                    "text": "All Platforms",
                    "additionalFields": {
                      "callback_data": "=publishall:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "Extend Video (+8s)",
                    "additionalFields": {
                      "callback_data": "=extend:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "Cancel",
                    "additionalFields": {
                      "callback_data": "=cancel:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "caption": "=**Video Ready!**\n\n**Title:** {{ $('Create Metadata').first().json.youtubeMetadata.title }}\n",
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a64aabf2-f9b9-472b-a7ed-af09abff099f",
      "name": "Send Video Preview (Other)",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2032,
        480
      ],
      "parameters": {
        "chatId": "={{ $('Create Metadata').first().json.chatId }}",
        "operation": "sendVideo",
        "binaryData": true,
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "YouTube",
                    "additionalFields": {
                      "callback_data": "=publish:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  },
                  {
                    "text": "TikTok",
                    "additionalFields": {
                      "callback_data": "=tiktok:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "Instagram",
                    "additionalFields": {
                      "callback_data": "=instagram:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  },
                  {
                    "text": "All Platforms",
                    "additionalFields": {
                      "callback_data": "=publishall:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "Cancel",
                    "additionalFields": {
                      "callback_data": "=cancel:{{ $('Create Metadata').first().json.sessionId }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "caption": "=**Video Ready!**\n\n**Title:** {{ $('Create Metadata').first().json.youtubeMetadata.title }}\n",
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "75afd09e-e746-449d-9c05-80f4f354bb11",
      "name": "Can Retry?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1120,
        720
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.attempt < 15 }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "fa1860dd-48a5-4174-b33e-eadf07e6ee23",
      "name": "Retry Poll",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -880,
        640
      ],
      "parameters": {
        "url": "https://your-n8n-instance.com/webhook/video-poll",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({\n  sessionId: $json.sessionId,\n  taskId: $json.taskId,\n  chatId: $json.chatId,\n  messageId: $json.messageId,\n  selectedModel: $json.selectedModel,\n  modelType: $json.modelType,\n  statusEndpoint: $json.statusEndpoint,\n  attempt: $json.attempt + 1\n}) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "0fa23fe4-5fe5-4c82-9e5d-06e9650225bc",
      "name": "Send Timeout",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -880,
        800
      ],
      "parameters": {
        "text": "=**Video Generation Timeout**\n\nThe video is taking longer than expected ({{ $json.attempt }} attempts).\n\nPlease try again later or send a new idea.\n\n_Task ID: {{ $json.taskId }}_",
        "chatId": "={{ $json.chatId }}",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6379dbc9-ddcc-4a67-aba4-d4b5c5c1faf0",
      "name": "Upload a file",
      "type": "n8n-nodes-base.s3",
      "position": [
        -432,
        384
      ],
      "parameters": {
        "fileName": "=videos/{{ $('Get Final URL').first().json.sessionId }}.mp4",
        "operation": "upload",
        "bucketName": "shorts",
        "additionalFields": {}
      },
      "credentials": {
        "s3": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3cdb8933-6193-469c-bd59-ddd6fbcc1fb1",
      "name": "Create Metadata",
      "type": "n8n-nodes-base.code",
      "position": [
        912,
        384
      ],
      "parameters": {
        "jsCode": "const session = $input.first().json;\n\nconst metadata = `Video Metadata\n==============\nSession ID: ${session.sessionId}\nGenerated: ${session.createdAt || 'N/A'}\nCompleted: ${new Date().toISOString()}\n\nOriginal Input\n--------------\nText: ${session.originalInput?.text || 'N/A'}\nPhoto: ${session.originalInput?.photoUrl ? 'Yes' : 'No'}\n\nVideo Generation\n----------------\nModel: ${session.selectedModel || 'veo3'}\nModel Name: ${session.modelName || 'Veo 3.1'}\nTask ID: ${session.taskId || 'N/A'}\nTemplate: ${session.template || 'general'}\nBalance at Generation: ${session.balanceAtGeneration || 'N/A'} credits\n\nYouTube Metadata\n----------------\nTitle: ${session.youtubeMetadata?.title || 'N/A'}\nDescription: ${session.youtubeMetadata?.description || 'N/A'}\nTags: ${session.youtubeMetadata?.tags?.join(', ') || 'N/A'}\n\nGenerated Prompt\n----------------\n${session.generatedPrompt || 'N/A'}\n\nURLs\n----\nS3: ${session.s3Url || 'N/A'}\nKIE: ${session.videoUrl || 'N/A'}`;\n\n// Convert text to binary for S3 upload\nconst binaryData = Buffer.from(metadata, 'utf-8');\n\nreturn {\n  json: {\n    ...session,\n    metadataKey: `videos/${session.sessionId}_metadata.txt`\n  },\n  binary: {\n    metadata: {\n      data: binaryData.toString('base64'),\n      mimeType: 'text/plain',\n      fileName: `${session.sessionId}_metadata.txt`\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "676e3f5f-2994-49b7-a19b-1065cf1623f4",
      "name": "Upload Metadata",
      "type": "n8n-nodes-base.s3",
      "position": [
        1136,
        384
      ],
      "parameters": {
        "fileName": "={{ $json.metadataKey }}",
        "operation": "upload",
        "bucketName": "shorts",
        "additionalFields": {},
        "binaryPropertyName": "metadata"
      },
      "credentials": {
        "s3": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6253a26f-4c47-4452-ade7-691dca7d2b9f",
      "name": "Is Extend?",
      "type": "n8n-nodes-base.if",
      "position": [
        -208,
        384
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $('Get Final URL').first().json.isExtend }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9f7d4cf8-9b9e-400a-82d3-3c00bda97742",
      "name": "Skip Concat",
      "type": "n8n-nodes-base.code",
      "position": [
        16,
        480
      ],
      "parameters": {
        "jsCode": "// Pass through for non-extend videos\nconst inputData = $('Get Final URL').first().json;\nreturn {\n  json: {\n    sessionId: inputData.sessionId,\n    isExtend: false,\n    videoUrl: inputData.videoUrl,\n    rawResponse: inputData.rawResponse\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "021a9e58-04b5-47d9-b4aa-d6d3e402df02",
      "name": "Merge After Concat",
      "type": "n8n-nodes-base.merge",
      "position": [
        240,
        384
      ],
      "parameters": {},
      "typeVersion": 2
    },
    {
      "id": "3f7921d2-1352-4bd4-a8b1-8740d39f6b21",
      "name": "Send Merge Options",
      "type": "n8n-nodes-base.telegram",
      "position": [
        16,
        288
      ],
      "parameters": {
        "text": "=**Extended Video Ready!** \ud83c\udfac\n\nYour video has been extended by ~8 seconds.\n\n**Choose how to merge:**",
        "chatId": "={{ $('Parse Input').first().json.chatId }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "\ud83d\udd04 Auto Merge (Transloadit)",
                    "additionalFields": {
                      "callback_data": "=am:{{ $('Get Final URL').first().json.sessionId }}"
                    }
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "\ud83d\udd17 Get Video Links (Manual)",
                    "additionalFields": {
                      "callback_data": "=mm:{{ $('Get Final URL').first().json.sessionId }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    }
  ],
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "1ea2783d-6705-40e7-897d-1135e709e438",
  "connections": {
    "Can Retry?": {
      "main": [
        [
          {
            "node": "Retry Poll",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Timeout",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Completed?": {
      "main": [
        [
          {
            "node": "Get 1080p Video",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Can Retry?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Extend?": {
      "main": [
        [
          {
            "node": "Send Merge Options",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip Concat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond OK": {
      "main": [
        [
          {
            "node": "Wait 1 Minute",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Session": {
      "main": [
        [
          {
            "node": "Merge Session Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Input": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip Concat": {
      "main": [
        [
          {
            "node": "Merge After Concat",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Parse Status": {
      "main": [
        [
          {
            "node": "Completed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Trigger": {
      "main": [
        [
          {
            "node": "Parse Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Final URL": {
      "main": [
        [
          {
            "node": "Download Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Veo Model?": {
      "main": [
        [
          {
            "node": "Send Video Preview (Veo)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Video Preview (Other)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload a file": {
      "main": [
        [
          {
            "node": "Is Extend?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 1 Minute": {
      "main": [
        [
          {
            "node": "Check KIE Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Video": {
      "main": [
        [
          {
            "node": "Upload a file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Session": {
      "main": [
        [
          {
            "node": "Download for Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Metadata": {
      "main": [
        [
          {
            "node": "Upload Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get 1080p Video": {
      "main": [
        [
          {
            "node": "Get Final URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Metadata": {
      "main": [
        [
          {
            "node": "Update Session",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check KIE Status": {
      "main": [
        [
          {
            "node": "Parse Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge After Concat": {
      "main": [
        [
          {
            "node": "Get Session",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Session Data": {
      "main": [
        [
          {
            "node": "Create Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Merge Options": {
      "main": [
        [
          {
            "node": "Merge After Concat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download for Telegram": {
      "main": [
        [
          {
            "node": "Is Veo Model?",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

[](https://www.linkedin.com/in/mosaab-yassir-lafrimi/)[](https://t.me/joevenner)

Source: https://n8n.io/workflows/12684/ — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Slack & Telegram

Telegram Wait. Uses stickyNote, httpRequest, redis, noOp. Event-driven trigger; 36 nodes.

HTTP Request, Redis, Telegram +1
Slack & Telegram

How it works • Webhook triggers from content creation system in Airtable • Downloads media (images/videos) from Airtable URLs • Uploads media to Postiz cloud storage • Schedules or publishes content a

Airtable, Telegram, HTTP Request
Slack & Telegram

I wanted to avoid the rush at end of month to log expenses. I tried existing expense apps but found them either too expensive for what they offer, or frustrating with inconsistent extraction results.

HTTP Request, Telegram
Slack & Telegram

This workflow is an AI-assisted clean plate and object removal pipeline built for modern VFX production environments. It transforms a single plate image and removal brief into multiple high-quality cl

HTTP Request, Google Drive, Slack +3
Slack & Telegram

Splitout. Uses stickyNote, respondToWebhook, httpRequest, splitInBatches. Webhook trigger; 29 nodes.

HTTP Request, S3, Slack