This workflow follows the Emailsend → 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": "Storyboard I2V Pipeline (17 shots) with Retry/Wait/State/Notify",
"nodes": [
{
"id": "Trigger_In",
"name": "HTTP Trigger (POST /i2v)",
"type": "n8n-nodes-base.httpTrigger",
"typeVersion": 1,
"position": [
200,
200
],
"parameters": {
"path": "i2v",
"options": {
"response": {
"response": "onReceived",
"responseCode": 200,
"responseData": "Received. Processing started."
}
}
}
},
{
"id": "Parse_Input",
"name": "Parse Input JSON",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
420,
200
],
"parameters": {
"functionCode": "const body = items[0].json;\n// \u671f\u671b\u8f93\u5165: { shots:[{id,image_prompt,video_prompt,durationSec}], options:{maxPolls,pollWaitSec,maxRetries} }\nif (!body.shots || !Array.isArray(body.shots) || body.shots.length === 0) {\n throw new Error('shots \u6570\u7ec4\u7f3a\u5931\u6216\u4e3a\u7a7a');\n}\nconst defaults = { maxPolls: 10, pollWaitSec: 20, maxRetries: 2 };\nconst options = Object.assign({}, defaults, body.options || {});\nreturn [{ json: { shots: body.shots, options, projectId: body.project_id || body.projectId || 'project-unknown', callbackUrl: body.callback_url || body.callbackUrl || '' } }];"
}
},
{
"id": "Split_Shots",
"name": "Split In Batches (Shots)",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 2,
"position": [
640,
200
],
"parameters": {
"options": {
"batchSize": 1
}
}
},
{
"id": "Set_Shot",
"name": "Set Current Shot",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
860,
200
],
"parameters": {
"functionCode": "const { shots, options, projectId, callbackUrl } = $items('Parse Input JSON')[0].json;\nconst current = $items('Split In Batches (Shots)').json;\nreturn [{ json: { shot: current, options, projectId, callbackUrl, __ctx: { imagePolls: 0, videoPolls: 0 } } }];"
}
},
{
"id": "Image_Create",
"name": "HTTP Image Create (replace with your model)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
1080,
140
],
"parameters": {
"url": "https://api.replicate.com/v1/predictions",
"method": "POST",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"jsonParameters": true,
"headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\", \"Content-Type\": \"application/json\" }",
"bodyParametersJson": "{\n \"version\": \"YOUR_IMAGE_MODEL_VERSION\",\n \"input\": {\n \"prompt\": \"{{$json.shot.image_prompt}}\",\n \"seed\": 12345,\n \"output_format\": \"png\"\n }\n}"
},
"notesInFlow": true,
"notes": "\u5c06 URL/Headers/Body \u66ff\u6362\u4e3a\u4f60\u7684\u56fe\u7247\u751f\u6210\u670d\u52a1\uff08\u5982\u8c46\u53053.0\u7b49\uff09\u3002"
},
{
"id": "Image_Status",
"name": "Check Image Status",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
1300,
140
],
"parameters": {
"url": "={{$json['urls'] && $json['urls']['get'] ? $json['urls']['get'] : $json['id'] ? 'https://api.replicate.com/v1/predictions/' + $json['id'] : ''}}",
"method": "GET",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"jsonParameters": true,
"headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\" }"
}
},
{
"id": "Image_Switch",
"name": "Switch Image State",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
1520,
140
],
"parameters": {
"dataType": "string",
"value1": "={{$json.status}}",
"rules": {
"rules": [
{
"operation": "equal",
"value": "succeeded"
},
{
"operation": "equal",
"value": "failed"
},
{
"operation": "contains",
"value": "processing"
}
]
}
}
},
{
"id": "Image_Wait",
"name": "Wait (Image Poll)",
"type": "n8n-nodes-base.wait",
"typeVersion": 1,
"position": [
1740,
80
],
"parameters": {
"waitTill": "timeInterval",
"timeInterval": {
"unit": "seconds",
"value": "={{$items('Parse Input JSON')[0].json.options.pollWaitSec}}"
}
}
},
{
"id": "Image_Poll_Count",
"name": "Inc Image Poll Count",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1940,
80
],
"parameters": {
"functionCode": "const ctx = $json.__ctx || { imagePolls: 0, videoPolls: 0 };\nctx.imagePolls += 1;\nreturn [{ json: Object.assign({}, $json, { __ctx: ctx }) }];"
}
},
{
"id": "Image_Poll_Limit",
"name": "Switch Image Poll Limit",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
2140,
80
],
"parameters": {
"dataType": "number",
"value1": "={{$json.__ctx.imagePolls}}",
"rules": {
"rules": [
{
"operation": "smaller",
"value": "={{$items('Parse Input JSON')[0].json.options.maxPolls}}"
},
{
"operation": "largerEqual",
"value": "={{$items('Parse Input JSON').json.options.maxPolls}}"
}
]
}
}
},
{
"id": "Image_Requery",
"name": "Re-Check Image Status",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
2340,
40
],
"parameters": {
"url": "={{$items('Check Image Status').json.id ? 'https://api.replicate.com/v1/predictions/' + $items('Check Image Status').json.id : ''}}",
"method": "GET",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"jsonParameters": true,
"headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\" }"
}
},
{
"id": "Image_Poll_Exceeded",
"name": "Notify Image Timeout",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2,
"position": [
2340,
120
],
"parameters": {
"fromEmail": "pipeline@studio.local",
"toEmail": "producer@studio.local",
"subject": "Image generation timeout",
"text": "Shot {{$items('Set Current Shot')[0].json.shot.id}} image generation timeout after {{$items('Parse Input JSON').json.options.maxPolls}} polls."
}
},
{
"id": "Image_Failed",
"name": "Notify Image Failed",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2,
"position": [
1740,
200
],
"parameters": {
"fromEmail": "pipeline@studio.local",
"toEmail": "producer@studio.local",
"subject": "Image generation failed",
"text": "Shot {{$items('Set Current Shot')[0].json.shot.id}} image failed. Please review logs and trigger manual retry if needed."
}
},
{
"id": "Image_Succeeded",
"name": "Set Image URL",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1740,
260
],
"parameters": {
"functionCode": "const out = items.json;\nconst imageUrl = Array.isArray(out.output) ? out.output : out.output || out.urls?.get || '';\nreturn [{ json: Object.assign({}, out, { imageUrl }) }];"
}
},
{
"id": "Video_Create",
"name": "HTTP Video Create (replace with your model)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
1960,
260
],
"parameters": {
"url": "https://api.replicate.com/v1/predictions",
"method": "POST",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"jsonParameters": true,
"headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\", \"Content-Type\": \"application/json\" }",
"bodyParametersJson": "{\n \"version\": \"YOUR_I2V_MODEL_VERSION\",\n \"input\": {\n \"prompt\": \"{{$items(\\\"Set Current Shot\\\")[0].json.shot.video_prompt}}\",\n \"image\": \"{{$items(\\\"Set Image URL\\\").json.imageUrl}}\",\n \"duration\": {{$items(\\\"Set Current Shot\\\").json.shot.durationSec || 3}}\n }\n}"
},
"notesInFlow": true,
"notes": "\u66ff\u6362\u4e3a\u5373\u68a63.0/Stable Video Diffusion/Wan 2.2 I2V \u7684\u521b\u5efa\u63a5\u53e3\u3002"
},
{
"id": "Video_Status",
"name": "Check Video Status",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
2180,
260
],
"parameters": {
"url": "={{$json['urls'] && $json['urls']['get'] ? $json['urls']['get'] : $json['id'] ? 'https://api.replicate.com/v1/predictions/' + $json['id'] : ''}}",
"method": "GET",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"jsonParameters": true,
"headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\" }"
}
},
{
"id": "Video_Switch",
"name": "Switch Video State",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
2400,
260
],
"parameters": {
"dataType": "string",
"value1": "={{$json.status}}",
"rules": {
"rules": [
{
"operation": "equal",
"value": "succeeded"
},
{
"operation": "equal",
"value": "failed"
},
{
"operation": "contains",
"value": "processing"
}
]
}
}
},
{
"id": "Video_Wait",
"name": "Wait (Video Poll)",
"type": "n8n-nodes-base.wait",
"typeVersion": 1,
"position": [
2620,
200
],
"parameters": {
"waitTill": "timeInterval",
"timeInterval": {
"unit": "seconds",
"value": "={{$items('Parse Input JSON')[0].json.options.pollWaitSec}}"
}
}
},
{
"id": "Video_Poll_Count",
"name": "Inc Video Poll Count",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
2820,
200
],
"parameters": {
"functionCode": "const ctx = $json.__ctx || { imagePolls: 0, videoPolls: 0 };\nctx.videoPolls += 1;\nreturn [{ json: Object.assign({}, $json, { __ctx: ctx }) }];"
}
},
{
"id": "Video_Poll_Limit",
"name": "Switch Video Poll Limit",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
3020,
200
],
"parameters": {
"dataType": "number",
"value1": "={{$json.__ctx.videoPolls}}",
"rules": {
"rules": [
{
"operation": "smaller",
"value": "={{$items('Parse Input JSON')[0].json.options.maxPolls}}"
},
{
"operation": "largerEqual",
"value": "={{$items('Parse Input JSON').json.options.maxPolls}}"
}
]
}
}
},
{
"id": "Video_Requery",
"name": "Re-Check Video Status",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
3220,
160
],
"parameters": {
"url": "={{$items('Check Video Status').json.id ? 'https://api.replicate.com/v1/predictions/' + $items('Check Video Status').json.id : ''}}",
"method": "GET",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"jsonParameters": true,
"headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\" }"
}
},
{
"id": "Video_Poll_Exceeded",
"name": "Notify Video Timeout",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2,
"position": [
3220,
240
],
"parameters": {
"fromEmail": "pipeline@studio.local",
"toEmail": "producer@studio.local",
"subject": "Video generation timeout",
"text": "Shot {{$items('Set Current Shot')[0].json.shot.id}} video generation timeout after {{$items('Parse Input JSON').json.options.maxPolls}} polls."
}
},
{
"id": "Video_Failed",
"name": "Notify Video Failed",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2,
"position": [
2620,
320
],
"parameters": {
"fromEmail": "pipeline@studio.local",
"toEmail": "producer@studio.local",
"subject": "Video generation failed",
"text": "Shot {{$items('Set Current Shot')[0].json.shot.id}} video failed. Please review logs."
}
},
{
"id": "Video_Succeeded",
"name": "Set Video URL",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
2620,
380
],
"parameters": {
"functionCode": "const out = items.json;\nconst videoUrl = Array.isArray(out.output) ? out.output : out.output || out.urls?.get || '';\nreturn [{ json: Object.assign({}, out, { videoUrl }) }];"
}
},
{
"id": "Aggregate_Result",
"name": "Aggregate Shot Result",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
2840,
380
],
"parameters": {
"functionCode": "const shot = $items('Set Current Shot')[0].json.shot;\nconst imageUrl = $items('Set Image URL').json.imageUrl || '';\nconst videoUrl = $items('Set Video URL').json.videoUrl || '';\nreturn [{ json: { id: shot.id, imageUrl, videoUrl } }];"
}
},
{
"id": "Continue_Batch",
"name": "Continue Batch",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 2,
"position": [
3060,
380
],
"parameters": {
"options": {
"continue": true
}
}
},
{
"id": "Collect_All",
"name": "Collect All Results",
"type": "n8n-nodes-base.merge",
"typeVersion": 2,
"position": [
3280,
380
],
"parameters": {
"mode": "passThrough",
"combine": "mergeByIndex"
}
},
{
"id": "Return_Final",
"name": "Respond JSON",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
3500,
380
],
"parameters": {
"responseBody": "={{ { projectId: $items('Parse Input JSON')[0].json.projectId, shots: $items('Collect All Results').map(i => i.json) } }}",
"responseCode": 200
}
}
],
"connections": {
"Parse Input JSON": {
"main": [
[
{
"node": "Split In Batches (Shots)",
"type": "main",
"index": 0
}
]
]
},
"HTTP Trigger (POST /i2v)": {
"main": [
[
{
"node": "Parse Input JSON",
"type": "main",
"index": 0
}
]
]
},
"Split In Batches (Shots)": {
"main": [
[
{
"node": "Set Current Shot",
"type": "main",
"index": 0
}
],
[
{
"node": "Collect All Results",
"type": "main",
"index": 0
}
]
]
},
"Set Current Shot": {
"main": [
[
{
"node": "HTTP Image Create (replace with your model)",
"type": "main",
"index": 0
}
]
]
},
"HTTP Image Create (replace with your model)": {
"main": [
[
{
"node": "Check Image Status",
"type": "main",
"index": 0
}
]
]
},
"Check Image Status": {
"main": [
[
{
"node": "Switch Image State",
"type": "main",
"index": 0
}
]
]
},
"Switch Image State": {
"main": [
[
{
"node": "Set Image URL",
"type": "main",
"index": 0
}
],
[
{
"node": "Notify Image Failed",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait (Image Poll)",
"type": "main",
"index": 0
}
]
]
},
"Wait (Image Poll)": {
"main": [
[
{
"node": "Inc Image Poll Count",
"type": "main",
"index": 0
}
]
]
},
"Inc Image Poll Count": {
"main": [
[
{
"node": "Switch Image Poll Limit",
"type": "main",
"index": 0
}
]
]
},
"Switch Image Poll Limit": {
"main": [
[
{
"node": "Re-Check Image Status",
"type": "main",
"index": 0
}
],
[
{
"node": "Notify Image Timeout",
"type": "main",
"index": 0
}
]
]
},
"Set Image URL": {
"main": [
[
{
"node": "HTTP Video Create (replace with your model)",
"type": "main",
"index": 0
}
]
]
},
"HTTP Video Create (replace with your model)": {
"main": [
[
{
"node": "Check Video Status",
"type": "main",
"index": 0
}
]
]
},
"Check Video Status": {
"main": [
[
{
"node": "Switch Video State",
"type": "main",
"index": 0
}
]
]
},
"Switch Video State": {
"main": [
[
{
"node": "Set Video URL",
"type": "main",
"index": 0
}
],
[
{
"node": "Notify Video Failed",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait (Video Poll)",
"type": "main",
"index": 0
}
]
]
},
"Wait (Video Poll)": {
"main": [
[
{
"node": "Inc Video Poll Count",
"type": "main",
"index": 0
}
]
]
},
"Inc Video Poll Count": {
"main": [
[
{
"node": "Switch Video Poll Limit",
"type": "main",
"index": 0
}
]
]
},
"Switch Video Poll Limit": {
"main": [
[
{
"node": "Re-Check Video Status",
"type": "main",
"index": 0
}
],
[
{
"node": "Notify Video Timeout",
"type": "main",
"index": 0
}
]
]
},
"Set Video URL": {
"main": [
[
{
"node": "Aggregate Shot Result",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Shot Result": {
"main": [
[
{
"node": "Continue Batch",
"type": "main",
"index": 0
}
]
]
},
"Continue Batch": {
"main": [
[
{
"node": "Split In Batches (Shots)",
"type": "main",
"index": 1
}
]
]
},
"Collect All Results": {
"main": [
[
{
"node": "Respond JSON",
"type": "main",
"index": 0
}
]
]
}
},
"meta": {
"templateCredsSetupCompleted": false
},
"settings": {
"saveExecutionProgress": "DEFAULT",
"executionOrder": "v1"
},
"staticData": null,
"tags": [
{
"name": "i2v"
},
{
"name": "retry"
},
{
"name": "polling"
}
]
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Storyboard I2V Pipeline (17 shots) with Retry/Wait/State/Notify. Uses httpTrigger, httpRequest, emailSend. Event-driven trigger; 28 nodes.
Source: https://gist.github.com/orime/05f8e83516e3b822d3367d166238cc7c — 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.
The Sora 2 API allows seamless generation of CGI ads, turning text prompts into stunning videos. This workflow automates the entire process from video generation to upload, notification, and file shar
Create CGI ads effortlessly by integrating the Google Veo3 API for video generation and uploading to Google Drive with seamless email notifications. On form submission: Triggers the workflow when a fo
This workflow automates the process of generating videos using the Veo 3 Fast API, uploading the video to Google Drive, and notifying the user via email. All tasks are executed seamlessly, ensuring a
This workflow automates the process of sending voice calls for verification purposes and combines it with email verification. It uses the ClickSend API for voice calls and integrates with SMTP for ema
Automate downloading of Bilibili videos via the Bilibili Video Downloader API (RapidAPI), upload them to Google Drive, and notify users by email — all using n8n workflow automation.