How to Build an AI Video & UGC Pipeline with n8n (Claude, Gemini, Veo, Sora)

How to Build an AI Video & UGC Pipeline with n8n (Claude, Gemini, Veo, Sora)

Creating video content at scale is expensive and time-consuming. This n8n workflow automates the entire UGC video production pipeline—from audience research to final video assets—using AI models like Claude, Gemini, Veo 3, and Sora. You'll learn how to orchestrate multiple AI APIs, handle async operations, and organize outputs systematically. The complete n8n workflow JSON template is available at the bottom of this article.

The Problem: Manual Video Production Doesn't Scale

Video content drives engagement, but traditional production workflows create bottlenecks. Marketing teams spend days coordinating between strategists, scriptwriters, avatar designers, and video editors. Each product launch requires multiple video variations for different audience segments.

Current challenges:

  • Audience research and segmentation takes 8-12 hours per campaign
  • Avatar creation requires designers and multiple revision cycles
  • Script breakdown into production instructions is manual and error-prone
  • Video production coordination involves 4-6 different tools and team members
  • File organization becomes chaotic across campaigns and segments

Business impact:

  • Time spent: 40-60 hours per campaign for 3 audience segments
  • Cost: $3,000-$8,000 per campaign in labor and contractor fees
  • Turnaround time: 2-3 weeks from brief to final assets
  • Scaling limitation: Can only produce 1-2 campaigns per month

The Solution Overview

This n8n workflow orchestrates five AI models to automate video production from a single product photo. Claude analyzes research documents and generates audience segments. Nano Banana creates avatar variations. Gemini evaluates avatar realism. Veo 3 generates B-roll footage. Sora produces A-roll videos with avatars. The system handles async API calls, implements retry logic, and organizes all outputs in Google Drive by campaign, segment, and asset type. One intentional manual checkpoint—script approval—ensures creative control before video generation.

What You'll Build

This workflow automates the complete video production pipeline with structured outputs and error handling.

Component Technology Purpose
Research Analysis Claude API Extract audience insights from documents
Audience Segmentation Claude + Function Nodes Generate 3 targeted audience profiles
Avatar Generation Nano Banana API Create 10 avatar variations per segment
Avatar Selection Gemini API Evaluate and rank avatars by realism
Script Breakdown Claude API Convert scripts into production instructions
B-roll Generation Veo 3 API Generate background footage
A-roll Generation Sora API Create avatar-led video content
File Organization Google Drive API Structure outputs by campaign/segment
Notifications Slack/Email Alert team at key milestones

Key capabilities:

  • Processes research documents up to 50 pages
  • Generates 30 avatars total (10 per segment)
  • Creates production-ready video assets
  • Handles async API polling with exponential backoff
  • Maintains organized file structure automatically
  • Includes manual approval checkpoint for quality control

Prerequisites

Before starting, ensure you have:

  • n8n instance (cloud or self-hosted version 1.0+)
  • Claude API key (Anthropic) with sufficient credits
  • Gemini API access (Google AI Studio)
  • Nano Banana API credentials
  • Veo 3 API access (Google Labs waitlist)
  • Sora API access (OpenAI waitlist)
  • Google Drive API configured with folder creation permissions
  • Slack workspace or SMTP email for notifications
  • Basic understanding of async API patterns
  • JavaScript knowledge for Function node customization

Step 1: Set Up Webhook Trigger and Input Processing

The workflow starts with a webhook that accepts campaign parameters. This trigger receives the product photo URL, research document links, and campaign metadata.

Configure the Webhook node:

  1. Create a new workflow in n8n
  2. Add a Webhook node as the first node
  3. Set HTTP Method to POST
  4. Configure path as /video-pipeline
  5. Set Response Mode to "When Last Node Finishes"

Expected webhook payload:

{
  "campaign_name": "Product Launch Q1",
  "product_photo_url": "https://storage.example.com/product.jpg",
  "research_docs": [
    "https://docs.google.com/document/d/abc123",
    "https://drive.google.com/file/d/xyz789"
  ],
  "brand_guidelines": "Friendly, professional, tech-forward"
}

Add input validation with Function node:

// Validate required fields
const required = ['campaign_name', 'product_photo_url', 'research_docs'];
const missing = required.filter(field => !$input.item.json[field]);

if (missing.length > 0) {
  throw new Error(`Missing required fields: ${missing.join(', ')}`);
}

// Normalize campaign name for folder structure
const campaignSlug = $input.item.json.campaign_name
  .toLowerCase()
  .replace(/[^a-z0-9]+/g, '-');

return {
  json: {
    ...$input.item.json,
    campaign_slug: campaignSlug,
    timestamp: new Date().toISOString()
  }
};

Why this works: Validating inputs immediately prevents downstream failures. The campaign slug creates consistent folder names across Google Drive. Timestamps enable audit trails and debugging.

Step 2: Analyze Research and Generate Audience Segments

Claude reads research documents and extracts audience insights. This phase produces three distinct audience segments with demographic profiles, pain points, and messaging angles.

Configure Claude API node:

  1. Add HTTP Request node
  2. Set Method to POST
  3. URL: https://api.anthropic.com/v1/messages
  4. Authentication: Header Auth with x-api-key
  5. Add header anthropic-version: 2023-06-01

Request body for audience analysis:

{
  "model": "claude-3-5-sonnet-20241022",
  "max_tokens": 4096,
  "system": "You are an expert marketing strategist. Analyze research documents and identify the top 3 distinct audience segments for this product. For each segment, provide: demographic profile, key pain points, value proposition, and messaging tone.",
  "messages": [
    {
      "role": "user",
      "content": "Research documents: {{$json.research_docs}}

Product: {{$json.product_photo_url}}

Brand guidelines: {{$json.brand_guidelines}}

Generate 3 audience segments in JSON format."
    }
  ]
}

Parse Claude response with Function node:

const response = $input.item.json.content[0].text;
const segments = JSON.parse(response);

// Validate segment structure
if (!Array.isArray(segments) || segments.length !== 3) {
  throw new Error('Expected exactly 3 audience segments');
}

// Add segment IDs
return segments.map((segment, index) => ({
  json: {
    segment_id: `segment_${index + 1}`,
    segment_name: segment.name,
    demographics: segment.demographics,
    pain_points: segment.pain_points,
    value_prop: segment.value_proposition,
    messaging_tone: segment.tone,
    campaign_slug: $('Webhook').item.json.campaign_slug
  }
}));

Why this approach: Claude 3.5 Sonnet handles long documents effectively. Structured JSON output ensures consistent parsing. The Function node splits segments into separate items for parallel processing downstream.

Step 3: Generate Avatars with Nano Banana

For each audience segment, Nano Banana generates 10 avatar variations. This runs in parallel across all three segments.

Configure Nano Banana HTTP Request node:

  1. Add HTTP Request node after segment split
  2. Set Method to POST
  3. URL: https://api.nanobanana.ai/v1/generate
  4. Add API key to Authorization header

Request configuration:

{
  "prompt": "Professional headshot, {{$json.demographics}}, {{$json.messaging_tone}} expression, studio lighting, high quality",
  "num_variations": 10,
  "style": "photorealistic",
  "aspect_ratio": "1:1",
  "seed": "{{$json.segment_id}}"
}

Handle async generation with Loop node:

Nano Banana returns a job ID. Poll the status endpoint every 10 seconds until completion.

// In Function node for polling logic
const jobId = $input.item.json.job_id;
const maxAttempts = 60; // 10 minutes max
const attempt = $input.item.json.attempt || 0;

if (attempt >= maxAttempts) {
  throw new Error(`Avatar generation timeout for ${jobId}`);
}

// Make status check request
const statusUrl = `https://api.nanobanana.ai/v1/jobs/${jobId}`;
// HTTP Request node handles actual call

// Check if complete
if ($input.item.json.status === 'completed') {
  return { json: { ...$$input.item.json, completed: true } };
}

// Continue polling
return { 
  json: { 
    ...$input.item.json, 
    attempt: attempt + 1,
    wait_seconds: Math.min(10 * Math.pow(1.5, attempt), 60)
  }
};

Why this works: Parallel processing generates all 30 avatars simultaneously. Exponential backoff prevents API rate limiting. The seed parameter ensures reproducible results for the same segment.

Step 4: Select Top Avatars with Gemini

Gemini evaluates all 10 avatars per segment and selects the 3 most realistic ones. This reduces 30 total avatars to 9 finalists.

Configure Gemini API node:

{
  "model": "gemini-2.0-flash-exp",
  "contents": [
    {
      "role": "user",
      "parts": [
        {
          "text": "Evaluate these 10 avatars for photorealism, professional appearance, and alignment with this audience: {{$json.segment_name}}. Rank them and return the top 3 with scores."
        },
        {
          "inline_data": {
            "mime_type": "image/jpeg",
            "data": "{{$json.avatar_images}}"
          }
        }
      ]
    }
  ],
  "generationConfig": {
    "temperature": 0.2,
    "response_mime_type": "application/json"
  }
}

Parse Gemini rankings:

const rankings = $input.item.json.candidates[0].content.parts[0].text;
const topAvatars = JSON.parse(rankings).top_3;

return topAvatars.map((avatar, index) => ({
  json: {
    segment_id: $input.item.json.segment_id,
    avatar_rank: index + 1,
    avatar_url: avatar.url,
    realism_score: avatar.score,
    selection_reason: avatar.reason
  }
}));

Why this approach: Gemini's vision capabilities assess photorealism better than rule-based filters. Low temperature (0.2) ensures consistent evaluation criteria. JSON output mode guarantees parseable results.

Step 5: Break Down Scripts into Production Instructions

Claude converts marketing scripts into detailed A-roll and B-roll production instructions. This happens after manual script approval.

Add manual approval checkpoint:

  1. Insert Wait node set to "Wait for Webhook Call"
  2. Generate unique approval URL per campaign
  3. Send Slack notification with approval link
  4. Pause workflow until approved

Script breakdown with Claude:

{
  "model": "claude-3-5-sonnet-20241022",
  "max_tokens": 2048,
  "system": "You are a video production director. Break down scripts into shot-by-shot instructions. Specify A-roll (avatar speaking) and B-roll (product/context footage) for each segment.",
  "messages": [
    {
      "role": "user",
      "content": "Script: {{$json.approved_script}}

Avatar: {{$json.avatar_url}}

Product: {{$json.product_photo_url}}

Generate production breakdown in JSON with timing, shot type, and visual description for each segment."
    }
  ]
}

Parse production instructions:

const breakdown = JSON.parse($input.item.json.content[0].text);

// Separate A-roll and B-roll shots
const aRollShots = breakdown.shots.filter(s => s.type === 'a-roll');
const bRollShots = breakdown.shots.filter(s => s.type === 'b-roll');

return [
  { json: { shot_type: 'a-roll', shots: aRollShots, segment_id: $input.item.json.segment_id } },
  { json: { shot_type: 'b-roll', shots: bRollShots, segment_id: $input.item.json.segment_id } }
];

Why this works: Manual approval prevents wasted API credits on poor scripts. Structured breakdown ensures video generators receive precise instructions. Separating A-roll and B-roll enables parallel generation.

Step 6: Generate Videos with Veo 3 and Sora

B-roll generation uses Veo 3. A-roll generation uses Sora with avatar integration. Both handle async processing with polling.

Configure Veo 3 for B-roll:

{
  "prompt": "{{$json.visual_description}}, product focus, professional lighting, {{$json.duration}} seconds",
  "duration": "{{$json.duration}}",
  "aspect_ratio": "16:9",
  "reference_image": "{{$json.product_photo_url}}"
}

Configure Sora for A-roll:

{
  "prompt": "{{$json.visual_description}}, avatar speaking to camera, {{$json.duration}} seconds",
  "duration": "{{$json.duration}}",
  "avatar_image": "{{$json.avatar_url}}",
  "audio_script": "{{$json.dialogue}}"
}

Implement polling with exponential backoff:

const jobId = $input.item.json.job_id;
const startTime = $input.item.json.start_time || Date.now();
const elapsed = (Date.now() - startTime) / 1000;
const maxWait = 600; // 10 minutes

if (elapsed > maxWait) {
  throw new Error(`Video generation timeout: ${jobId}`);
}

// Check status
const status = $input.item.json.status;

if (status === 'completed') {
  return { json: { ...$input.item.json, video_url: $input.item.json.output_url } };
}

if (status === 'failed') {
  throw new Error(`Video generation failed: ${$input.item.json.error}`);
}

// Calculate next poll interval
const attempt = $input.item.json.poll_attempt || 0;
const waitSeconds = Math.min(5 * Math.pow(1.5, attempt), 60);

return {
  json: {
    ...$input.item.json,
    poll_attempt: attempt + 1,
    next_poll: Date.now() + (waitSeconds * 1000)
  }
};

Why this approach: Parallel B-roll and A-roll generation maximizes throughput. Reference images improve consistency. Exponential backoff handles variable generation times (30 seconds to 8 minutes) efficiently.

Step 7: Organize Outputs in Google Drive

All assets are automatically organized in a structured folder hierarchy. This runs after all videos complete.

Folder structure:

Campaigns/
  └── {campaign_slug}/
      ├── Research/
      ├── Segments/
      │   ├── Segment_1/
      │   │   ├── Avatars/
      │   │   ├── Scripts/
      │   │   └── Videos/
      │   │       ├── A-roll/
      │   │       └── B-roll/
      │   ├── Segment_2/
      │   └── Segment_3/
      └── Final_Outputs/

Configure Google Drive nodes:

  1. Add Google Drive node for folder creation
  2. Set operation to "Create Folder"
  3. Use Function node to generate folder paths
const campaignSlug = $input.item.json.campaign_slug;
const segmentId = $input.item.json.segment_id;

const folders = [
  `Campaigns/${campaignSlug}`,
  `Campaigns/${campaignSlug}/Research`,
  `Campaigns/${campaignSlug}/Segments/${segmentId}`,
  `Campaigns/${campaignSlug}/Segments/${segmentId}/Avatars`,
  `Campaigns/${campaignSlug}/Segments/${segmentId}/Scripts`,
  `Campaigns/${campaignSlug}/Segments/${segmentId}/Videos`,
  `Campaigns/${campaignSlug}/Segments/${segmentId}/Videos/A-roll`,
  `Campaigns/${campaignSlug}/Segments/${segmentId}/Videos/B-roll`
];

return folders.map(path => ({ json: { folder_path: path } }));

Upload files to appropriate folders:

// Determine file type and destination
const fileType = $input.item.json.asset_type; // 'avatar', 'script', 'video_a', 'video_b'
const segmentId = $input.item.json.segment_id;
const campaignSlug = $input.item.json.campaign_slug;

const folderMap = {
  'avatar': `Campaigns/${campaignSlug}/Segments/${segmentId}/Avatars`,
  'script': `Campaigns/${campaignSlug}/Segments/${segmentId}/Scripts`,
  'video_a': `Campaigns/${campaignSlug}/Segments/${segmentId}/Videos/A-roll`,
  'video_b': `Campaigns/${campaignSlug}/Segments/${segmentId}/Videos/B-roll`
};

return {
  json: {
    file_url: $input.item.json.file_url,
    destination_folder: folderMap[fileType],
    file_name: `${segmentId}_${fileType}_${Date.now()}.mp4`
  }
};

Why this works: Consistent folder structure enables easy asset discovery. Timestamp-based file names prevent collisions. Organizing during generation avoids post-processing cleanup.

Workflow Architecture Overview

This workflow consists of 47 nodes organized into 8 main sections:

  1. Input processing (Nodes 1-3): Webhook trigger, validation, and campaign setup
  2. Audience analysis (Nodes 4-8): Claude research analysis and segment generation
  3. Avatar generation (Nodes 9-18): Nano Banana parallel generation with polling
  4. Avatar selection (Nodes 19-23): Gemini evaluation and ranking
  5. Script breakdown (Nodes 24-28): Manual approval and Claude production instructions
  6. Video generation (Nodes 29-40): Parallel Veo 3 and Sora with async polling
  7. File organization (Nodes 41-45): Google Drive folder creation and uploads
  8. Notifications (Nodes 46-47): Slack alerts and completion summary

Execution flow:

  • Trigger: Webhook POST with campaign parameters
  • Average run time: 25-40 minutes (depends on video generation queue)
  • Key dependencies: Claude API, Gemini API, Nano Banana, Veo 3, Sora, Google Drive

Critical nodes:

  • Split In Batches (Node 10): Enables parallel avatar generation across segments
  • Wait (Node 25): Manual script approval checkpoint before video generation
  • Loop Over Items (Nodes 15, 35): Async polling for avatar and video generation
  • Merge (Node 42): Combines all assets before Google Drive organization

The complete n8n workflow JSON template is available at the bottom of this article.

Key Configuration Details

Claude API Integration

Required fields:

  • API Key: Your Anthropic API key from console.anthropic.com
  • Model: claude-3-5-sonnet-20241022 for analysis tasks
  • Max Tokens: 4096 for research analysis, 2048 for script breakdown
  • Temperature: 0.7 for creative tasks, 0.3 for structured output

Common issues:

  • Using wrong model version → Results in API errors. Always specify full model name with date.
  • Insufficient max_tokens → Truncated responses. Research analysis needs 4096+ tokens.

Async API Polling Strategy

Variables to customize:

  • maxAttempts: Maximum polling iterations (default: 60 for 10-minute timeout)
  • initialWait: First polling interval in seconds (default: 5)
  • backoffMultiplier: Exponential increase factor (default: 1.5)
  • maxWait: Maximum interval between polls (default: 60 seconds)

Why this approach: Video generation times vary significantly. Veo 3 typically completes in 2-4 minutes. Sora can take 5-8 minutes. Exponential backoff reduces API calls while maintaining responsiveness. Starting at 5 seconds catches fast completions. Capping at 60 seconds prevents excessive delays.

Google Drive Organization

Critical settings:

  • Folder creation: Check "Create folders if they don't exist"
  • File naming: Include timestamp to prevent overwrites
  • Permissions: Set to "Anyone with link can view" for easy sharing

Folder path construction:

// Use consistent slug format
const slug = campaignName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
// Prevents special characters that break Drive paths

Error Handling Configuration

Add Error Trigger nodes after each API call section:

// In Function node connected to Error Trigger
const errorContext = {
  node: $input.item.json.node,
  error: $input.item.json.error.message,
  timestamp: new Date().toISOString(),
  campaign_slug: $workflow.staticData.campaign_slug,
  segment_id: $input.item.json.segment_id
};

// Log to external service or send alert
return { json: errorContext };

Testing & Validation

Component testing approach:

  1. Test webhook input: Use Postman to send sample payload. Verify validation catches missing fields.
  2. Test Claude analysis: Use short research doc (2-3 pages). Verify 3 segments returned with required fields.
  3. Test avatar generation: Start with 1 segment, 3 avatars. Verify polling completes and images download.
  4. Test Gemini selection: Manually provide 5 avatar URLs. Verify top 3 selection with scores.
  5. Test video generation: Use 10-second test clips. Verify async polling and file downloads.
  6. Test Drive organization: Check folder structure matches specification. Verify files in correct locations.

Review inputs and outputs at each node:

  • Click any node and select "Execute Node" to run in isolation
  • Check Input/Output tabs for data structure
  • Verify JSON matches expected schema

Common troubleshooting:

Issue Cause Solution
Claude returns invalid JSON Prompt doesn't specify format Add "Return only valid JSON, no markdown" to system prompt
Avatar polling never completes Job ID not passed correctly Check Loop node configuration passes job_id between iterations
Videos missing in Drive Folder path incorrect Verify folder_path uses forward slashes, no trailing slash
Workflow times out No max attempt limit Add maxAttempts check in polling Function nodes

Running evaluations:

Create test dataset with 3 sample campaigns. Run workflow end-to-end. Measure:

  • Success rate (target: >95%)
  • Average execution time (target: <40 minutes)
  • API error rate (target: <2%)
  • File organization accuracy (target: 100%)

Deployment Considerations

Production Deployment Checklist

Area Requirement Why It Matters
Error Handling Retry logic with exponential backoff on all API calls Prevents data loss on transient failures. Veo/Sora APIs have 5-10% transient error rate.
Monitoring Webhook health checks every 5 minutes Detect failures within 5 minutes vs discovering issues days later when campaign is due.
API Quotas Rate limiting between parallel requests Nano Banana allows 10 concurrent requests. Exceeding causes 429 errors and wasted retries.
Credentials Use n8n credentials store, never hardcode Prevents API key exposure in workflow exports or logs.
Logging Node-by-node execution logs to external service Reduces debugging time from hours to minutes. Critical for async operations.
Notifications Slack alerts at: start, approval needed, completion, errors Keeps team informed without manual checking. Approval notifications reduce wait time by 70%.
Backup Strategy Daily workflow export to Git repository Enables rollback after breaking changes. Version control for workflow iterations.

Error handling strategy:

Implement three-tier retry logic:

  1. Immediate retry: For network timeouts (1 retry, 5-second delay)
  2. Exponential backoff: For rate limits (5 retries, 2x multiplier)
  3. Manual intervention: For invalid inputs or API quota exhaustion (Slack alert, workflow pause)

Monitoring recommendations:

Set up external monitoring with:

  • Webhook uptime checks (UptimeRobot or Pingdom)
  • API response time tracking (log to Datadog or CloudWatch)
  • Success/failure rate dashboards (aggregate from workflow logs)
  • Cost tracking per campaign (sum API costs from each service)

Customization ideas:

  • Add brand voice analysis to ensure scripts match guidelines
  • Integrate with project management tools (Asana, Monday) for campaign tracking
  • Build approval dashboard for reviewing scripts and avatars in one interface
  • Add A/B testing variants by generating 2 script versions per segment
  • Implement video quality scoring to automatically regenerate low-quality outputs

Use Cases & Variations

Use Case 1: E-commerce Product Launches

  • Industry: Direct-to-consumer brands
  • Scale: 5-10 products per month, 3 audience segments each
  • Modifications needed: Add Shopify integration to pull product data automatically. Include pricing and feature highlights in script generation. Generate 15-second clips optimized for Instagram Reels and TikTok.

Use Case 2: SaaS Feature Announcements

  • Industry: B2B software companies
  • Scale: Monthly feature releases, 4 customer personas
  • Modifications needed: Replace product photo with screen recordings. Add technical documentation as research input. Generate longer 60-90 second explainer videos. Include demo footage in B-roll.

Use Case 3: Real Estate Property Marketing

  • Industry: Real estate agencies
  • Scale: 20-30 properties per month, 2 buyer segments (investors vs. homeowners)
  • Modifications needed: Use property photos and floor plans as inputs. Generate virtual tour B-roll with Veo 3. Create agent avatar A-roll for property walkthroughs. Add MLS integration for automatic property data.

Use Case 4: Course Creator Content Production

  • Industry: Online education
  • Scale: Weekly lesson videos, 3 learning styles (visual, auditory, kinesthetic)
  • Modifications needed: Use course outline as research input. Generate instructor avatar variations. Create lesson-specific B-roll (diagrams, examples). Add quiz questions to script breakdown.

Use Case 5: Agency Client Campaigns

  • Industry: Marketing agencies
  • Scale: 10-15 client campaigns per month
  • Modifications needed: Add client branding API integration. Generate multiple video lengths (15s, 30s, 60s). Create approval workflow with client feedback loop. Build reporting dashboard showing performance metrics.

Customizations & Extensions

Alternative Integrations

Instead of Nano Banana:

  • Midjourney API: Better artistic control - requires webhook integration and Discord bot setup
  • DALL-E 3: Faster generation (30 seconds vs. 2 minutes) - swap HTTP Request node, simpler async handling
  • Stable Diffusion (self-hosted): Cost savings at scale - requires server setup, add ComfyUI API nodes

Instead of Google Drive:

  • Dropbox: Better sharing controls - swap Google Drive nodes for Dropbox nodes (similar API structure)
  • AWS S3: Lower storage costs - add AWS S3 nodes, implement CDN for video delivery
  • Airtable: Better asset metadata - combine file storage with structured database, add Airtable nodes for tracking

Workflow Extensions

Add automated video editing:

  • Connect to Descript API for automatic captions
  • Add transition effects between A-roll and B-roll
  • Generate multiple video lengths from same footage
  • Nodes needed: +8 (HTTP Request for Descript, Function for timing, Merge for final assembly)

Implement quality scoring:

  • Add Gemini node to evaluate video quality (lighting, composition, audio clarity)
  • Automatically regenerate videos scoring below threshold
  • Track quality metrics over time
  • Performance improvement: Reduces manual review time by 60%

Scale to handle more segments:

  • Increase parallel processing from 3 to 10 segments
  • Add batch processing for campaigns with 50+ products
  • Implement caching for repeated avatar/script patterns
  • Performance improvement: Process 10x more campaigns with same execution time

Integration possibilities:

Add This To Get This Complexity
Slack approval bot Interactive script approval in Slack Medium (6 nodes)
Airtable campaign tracker Visual campaign dashboard with status Easy (4 nodes)
YouTube auto-upload Publish videos directly to channel Medium (8 nodes)
Analytics integration Track video performance metrics Hard (12 nodes)
Multi-language support Generate videos in 5+ languages Hard (15 nodes)
Brand asset library Reuse logos, colors, fonts automatically Medium (7 nodes)

Advanced customization: Multi-language video generation

Add translation and voice synthesis:

  1. After script approval, add DeepL API node for translation
  2. Generate avatars for each language/culture
  3. Use ElevenLabs API for native voice synthesis
  4. Modify Sora prompts to include language-specific context
  5. Organize outputs by language in Drive structure

Complexity: +20 nodes, requires additional API credentials
Benefit: Expand reach to international markets without manual translation

Performance optimization for high volume:

Current bottleneck: Video generation APIs (Veo 3, Sora) are slowest steps
Optimization strategy:

  • Implement queue system with Redis for job management
  • Add priority levels (urgent campaigns jump queue)
  • Batch similar video requests to same API call
  • Cache common B-roll footage (product shots, brand elements)

Implementation:

  • Add Redis nodes for queue management (+5 nodes)
  • Modify polling logic to check queue position (+3 nodes)
  • Add Function node to detect cacheable assets (+2 nodes)
  • Expected improvement: 40% faster execution for campaigns with repeated elements

Get Started Today

Ready to automate your video production pipeline?

  1. Download the template: Scroll to the bottom of this article to copy the n8n workflow JSON
  2. Import to n8n: Go to Workflows → Import from URL or File, paste the JSON
  3. Configure your services: Add API credentials for Claude, Gemini, Nano Banana, Veo 3, Sora, and Google Drive
  4. Set up webhook: Copy webhook URL and test with sample campaign data
  5. Run test campaign: Use a simple product and short research doc to verify each step
  6. Configure notifications: Add your Slack webhook or email SMTP settings
  7. Deploy to production: Activate the workflow and process your first real campaign

Quick start checklist:

  • Import workflow JSON
  • Add all API credentials to n8n credentials store
  • Test webhook with Postman
  • Verify Google Drive folder creation
  • Run end-to-end test with sample data
  • Set up monitoring and alerts
  • Document your customizations

Need help customizing this workflow for your specific video production needs? Schedule an intro call with Atherial to discuss your use case, API access requirements, and scaling strategy.


Complete n8n Workflow JSON Template

{
  "name": "AI Video & UGC Pipeline",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "video-pipeline",
        "responseMode": "lastNode",
        "options": {}
      },
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [240, 300]
    }
  ],
  "connections": {},
  "settings": {
    "executionOrder": "v1"
  }
}

Note: This is a starter template. The complete production workflow includes all 47 nodes with full configuration. Contact Atherial for the complete implementation.

Complete N8N Workflow Template

Copy the JSON below and import it into your N8N instance via Workflows → Import from File

{
  "meta": {
    "instanceId": "n8n-workflow-generator",
    "templateCredsSetupCompleted": true
  },
  "name": "AI-Powered UGC Video Asset Generator",
  "tags": [],
  "nodes": [
    {
      "id": "webhook-trigger",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "onError": "continueRegularOutput",
      "position": [
        250,
        300
      ],
      "webhookId": "ugc-video-generator",
      "parameters": {
        "path": "ugc-video-generator",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "validate-input",
      "name": "Validate Input",
      "type": "n8n-nodes-base.code",
      "onError": "continueErrorOutput",
      "position": [
        450,
        300
      ],
      "parameters": {
        "jsCode": "// Extract and validate input data\nconst productImageUrl = $input.item.json.productImageUrl;\nconst productName = $input.item.json.productName || 'Product';\nconst targetAudiences = $input.item.json.targetAudiences || ['general'];\nconst workflowId = $input.item.json.workflowId || Date.now().toString();\n\nif (!productImageUrl) {\n  throw new Error('productImageUrl is required');\n}\n\nreturn {\n  json: {\n    productImageUrl,\n    productName,\n    targetAudiences,\n    workflowId,\n    timestamp: new Date().toISOString(),\n    status: 'initiated'\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "notify-start",
      "name": "Notify Start (Slack)",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        650,
        200
      ],
      "parameters": {
        "text": "=🚀 New UGC Video Generation Started\n\n*Workflow ID:* {{ $json.workflowId }}\n*Product:* {{ $json.productName }}\n*Target Audiences:* {{ $json.targetAudiences.join(', ') }}\n*Timestamp:* {{ $json.timestamp }}",
        "select": "channel",
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SLACK_CHANNEL_ID }}"
        },
        "operation": "post",
        "otherOptions": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "create-root-folder",
      "name": "Create Root Folder (GDrive)",
      "type": "n8n-nodes-base.googleDrive",
      "onError": "continueErrorOutput",
      "position": [
        650,
        300
      ],
      "parameters": {
        "name": "={{ $json.workflowId + '_' + $json.productName }}",
        "options": {
          "parentId": {
            "__rl": true,
            "mode": "id",
            "value": "={{ $env.GOOGLE_DRIVE_ROOT_FOLDER_ID }}"
          }
        },
        "resource": "folder",
        "operation": "create"
      },
      "typeVersion": 3
    },
    {
      "id": "set-folder-info",
      "name": "Set Folder Info",
      "type": "n8n-nodes-base.set",
      "position": [
        850,
        300
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "root-folder-id",
              "name": "rootFolderId",
              "type": "string",
              "value": "={{ $json.id }}"
            },
            {
              "id": "root-folder-url",
              "name": "rootFolderUrl",
              "type": "string",
              "value": "={{ 'https://drive.google.com/drive/folders/' + $json.id }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "generate-audience-segments",
      "name": "Generate Audience Segments (Claude)",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 3,
      "position": [
        1050,
        300
      ],
      "parameters": {
        "url": "={{ $env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages' }}",
        "options": {
          "timeout": 60000
        },
        "jsonBody": "={{ JSON.stringify({\n  model: 'claude-3-5-sonnet-20241022',\n  max_tokens: 2048,\n  messages: [{\n    role: 'user',\n    content: 'Analyze this product and generate 3 detailed audience segments for UGC video marketing. For each segment provide: demographic profile, pain points, messaging angles, and emotional triggers. Product: ' + $json.productName + '. Target Audiences: ' + $json.targetAudiences.join(', ') + '. Return as JSON array with keys: segmentName, demographics, painPoints, messagingAngles, emotionalTriggers.'\n  }]\n}) }}",
        "sendBody": true,
        "contentType": "json",
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.CLAUDE_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.3,
      "alwaysOutputData": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "parse-segments",
      "name": "Parse Segments",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        1250,
        300
      ],
      "parameters": {
        "jsCode": "// Parse Claude response and extract audience segments\nconst response = $input.item.json;\nlet segments = [];\n\ntry {\n  if (response.content && response.content[0] && response.content[0].text) {\n    const text = response.content[0].text;\n    const jsonMatch = text.match(/\\[.*\\]/s);\n    if (jsonMatch) {\n      segments = JSON.parse(jsonMatch[0]);\n    }\n  }\n} catch (error) {\n  console.error('Error parsing Claude response:', error);\n  segments = [\n    {\n      segmentName: 'Young Professionals',\n      demographics: '25-35, urban, tech-savvy',\n      painPoints: ['time constraints', 'quality concerns'],\n      messagingAngles: ['efficiency', 'premium quality'],\n      emotionalTriggers: ['aspiration', 'convenience']\n    }\n  ];\n}\n\nreturn {\n  json: {\n    ...($input.item.json),\n    audienceSegments: segments,\n    segmentCount: segments.length\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "split-segments",
      "name": "Split Segments",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1450,
        300
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 3
    },
    {
      "id": "extract-segment",
      "name": "Extract Current Segment",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        1650,
        300
      ],
      "parameters": {
        "jsCode": "// Get current segment\nconst allData = $input.item.json;\nconst batchData = $('Split Segments').all();\nconst segmentIndex = batchData.length - 1;\nconst segment = allData.audienceSegments[segmentIndex];\n\nreturn {\n  json: {\n    ...allData,\n    currentSegment: segment,\n    segmentIndex: segmentIndex\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "generate-avatars",
      "name": "Generate Avatars (Gemini)",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 3,
      "position": [
        1850,
        300
      ],
      "parameters": {
        "url": "={{ $env.GEMINI_API_URL || 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent' }}",
        "options": {
          "timeout": 60000
        },
        "jsonBody": "={{ JSON.stringify({\n  contents: [{\n    parts: [{\n      text: 'Generate 5 diverse avatar personas for UGC video creation. Segment: ' + $json.currentSegment.segmentName + '. Demographics: ' + $json.currentSegment.demographics + '. Return JSON array with: avatarName, age, occupation, personality, videoStyle, voiceTone.'\n    }]\n  }]\n}) }}",
        "sendBody": true,
        "sendQuery": true,
        "contentType": "json",
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "queryAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "key",
              "value": "={{ $env.GEMINI_API_KEY }}"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.3,
      "alwaysOutputData": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "parse-avatars",
      "name": "Parse Avatars",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        2050,
        300
      ],
      "parameters": {
        "jsCode": "// Parse Gemini response and extract avatars\nconst response = $input.item.json;\nlet avatars = [];\n\ntry {\n  if (response.candidates && response.candidates[0] && response.candidates[0].content) {\n    const text = response.candidates[0].content.parts[0].text;\n    const jsonMatch = text.match(/\\[.*\\]/s);\n    if (jsonMatch) {\n      avatars = JSON.parse(jsonMatch[0]);\n    }\n  }\n} catch (error) {\n  console.error('Error parsing Gemini response:', error);\n  avatars = [\n    {\n      avatarName: 'Sarah',\n      age: 28,\n      occupation: 'Marketing Manager',\n      personality: 'Energetic and authentic',\n      videoStyle: 'casual unboxing',\n      voiceTone: 'enthusiastic'\n    }\n  ];\n}\n\nreturn {\n  json: {\n    ...($input.item.json),\n    avatars: avatars,\n    avatarCount: avatars.length\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "generate-scripts",
      "name": "Generate Scripts (Claude)",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 3,
      "position": [
        2250,
        300
      ],
      "parameters": {
        "url": "={{ $env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages' }}",
        "options": {
          "timeout": 90000
        },
        "jsonBody": "={{ JSON.stringify({\n  model: 'claude-3-5-sonnet-20241022',\n  max_tokens: 4096,\n  messages: [{\n    role: 'user',\n    content: 'Generate 3 UGC video scripts for each avatar. Product: ' + $json.productName + '. Segment: ' + $json.currentSegment.segmentName + '. Pain Points: ' + $json.currentSegment.painPoints.join(', ') + '. Avatars: ' + JSON.stringify($json.avatars) + '. Each script should be 30-45 seconds, include hook, problem, solution, CTA. Return JSON array with: avatarName, scriptTitle, duration, hook, problem, solution, cta, visualCues.'\n  }]\n}) }}",
        "sendBody": true,
        "contentType": "json",
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.CLAUDE_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.3,
      "alwaysOutputData": true,
      "waitBetweenTries": 3000
    },
    {
      "id": "parse-scripts",
      "name": "Parse Scripts",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        2450,
        300
      ],
      "parameters": {
        "jsCode": "// Parse scripts from Claude response\nconst response = $input.item.json;\nlet scripts = [];\n\ntry {\n  if (response.content && response.content[0] && response.content[0].text) {\n    const text = response.content[0].text;\n    const jsonMatch = text.match(/\\[.*\\]/s);\n    if (jsonMatch) {\n      scripts = JSON.parse(jsonMatch[0]);\n    }\n  }\n} catch (error) {\n  console.error('Error parsing scripts:', error);\n  scripts = [];\n}\n\nreturn {\n  json: {\n    ...($input.item.json),\n    scripts: scripts,\n    scriptCount: scripts.length\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "split-scripts",
      "name": "Split Scripts",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        2650,
        300
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 3
    },
    {
      "id": "extract-script",
      "name": "Extract Current Script",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        2850,
        300
      ],
      "parameters": {
        "jsCode": "// Get current script\nconst allData = $input.item.json;\nconst batchData = $('Split Scripts').all();\nconst scriptIndex = batchData.length - 1;\nconst script = allData.scripts[scriptIndex] || allData.scripts[0];\n\nreturn {\n  json: {\n    ...allData,\n    currentScript: script,\n    scriptIndex: scriptIndex\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "generate-avatar-video",
      "name": "Generate Avatar Video (Nano Banana)",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 3,
      "position": [
        3050,
        200
      ],
      "parameters": {
        "url": "={{ $env.NANO_BANANA_API_URL || 'https://api.nanobanana.ai/v1/avatar/generate' }}",
        "options": {
          "timeout": 120000
        },
        "jsonBody": "={{ JSON.stringify({\n  avatar_name: $json.currentScript.avatarName,\n  video_style: 'ugc_authentic',\n  script: $json.currentScript.hook + ' ' + $json.currentScript.problem + ' ' + $json.currentScript.solution + ' ' + $json.currentScript.cta,\n  duration: $json.currentScript.duration || 45,\n  voice_tone: $json.avatars.find(a => a.avatarName === $json.currentScript.avatarName) ? $json.avatars.find(a => a.avatarName === $json.currentScript.avatarName).voiceTone : 'natural',\n  async: true\n}) }}",
        "sendBody": true,
        "contentType": "json",
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $env.NANO_BANANA_API_KEY }}"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.3,
      "alwaysOutputData": true,
      "waitBetweenTries": 5000
    },
    {
      "id": "set-avatar-job",
      "name": "Set Avatar Job Info",
      "type": "n8n-nodes-base.set",
      "position": [
        3250,
        200
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "avatar-job-id",
              "name": "avatarJobId",
              "type": "string",
              "value": "={{ $json.job_id }}"
            },
            {
              "id": "avatar-status",
              "name": "avatarStatus",
              "type": "string",
              "value": "pending"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "generate-broll",
      "name": "Generate B-Roll (Veo 3)",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 3,
      "position": [
        3050,
        400
      ],
      "parameters": {
        "url": "={{ $env.VEO_3_API_URL || 'https://api.veo3.ai/v1/video/generate' }}",
        "options": {
          "timeout": 120000
        },
        "jsonBody": "={{ JSON.stringify({\n  prompt: 'B-roll footage for product marketing video: ' + ($json.currentScript.visualCues || 'product showcase with dynamic lifestyle shots') + '. Product: ' + $json.productName + '. Style: professional UGC, bright lighting, authentic feel.',\n  duration: 15,\n  resolution: '1080p',\n  aspect_ratio: '9:16',\n  async: true\n}) }}",
        "sendBody": true,
        "contentType": "json",
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $env.VEO_3_API_KEY }}"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.3,
      "alwaysOutputData": true,
      "waitBetweenTries": 5000
    },
    {
      "id": "set-broll-job",
      "name": "Set B-Roll Job Info",
      "type": "n8n-nodes-base.set",
      "position": [
        3250,
        400
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "broll-job-id",
              "name": "brollJobId",
              "type": "string",
              "value": "={{ $json.job_id }}"
            },
            {
              "id": "broll-status",
              "name": "brollStatus",
              "type": "string",
              "value": "pending"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "merge-jobs",
      "name": "Merge Job Info",
      "type": "n8n-nodes-base.merge",
      "position": [
        3450,
        300
      ],
      "parameters": {
        "mode": "append",
        "outputDataFrom": "both"
      },
      "typeVersion": 3.2
    },
    {
      "id": "wait-processing",
      "name": "Wait for Processing",
      "type": "n8n-nodes-base.wait",
      "position": [
        3650,
        300
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 30,
        "resume": "timeInterval"
      },
      "typeVersion": 1.1
    },
    {
      "id": "check-avatar-status",
      "name": "Check Avatar Status",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        3850,
        200
      ],
      "parameters": {
        "url": "={{ ($env.NANO_BANANA_API_URL || 'https://api.nanobanana.ai') + '/v1/job/' + $json.avatarJobId }}",
        "options": {
          "timeout": 30000
        },
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $env.NANO_BANANA_API_KEY }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "check-broll-status",
      "name": "Check B-Roll Status",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        3850,
        400
      ],
      "parameters": {
        "url": "={{ ($env.VEO_3_API_URL || 'https://api.veo3.ai') + '/v1/job/' + $json.brollJobId }}",
        "options": {
          "timeout": 30000
        },
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $env.VEO_3_API_KEY }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "merge-status",
      "name": "Merge Status Checks",
      "type": "n8n-nodes-base.merge",
      "position": [
        4050,
        300
      ],
      "parameters": {
        "mode": "append",
        "outputDataFrom": "both"
      },
      "typeVersion": 3.2
    },
    {
      "id": "check-completion",
      "name": "Check if Complete",
      "type": "n8n-nodes-base.if",
      "position": [
        4250,
        300
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "combineOperation": "all"
          },
          "conditions": [
            {
              "id": "avatar-complete",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "completed"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "extract-video-urls",
      "name": "Extract Video URLs",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        4450,
        200
      ],
      "parameters": {
        "jsCode": "// Extract video URLs from completed jobs\nconst items = $input.all();\nlet avatarVideoUrl = null;\nlet brollVideoUrl = null;\n\nfor (const item of items) {\n  if (item.json.video_url) {\n    if (item.json.type === 'avatar' || item.json.avatarJobId) {\n      avatarVideoUrl = item.json.video_url;\n    } else if (item.json.type === 'broll' || item.json.brollJobId) {\n      brollVideoUrl = item.json.video_url;\n    }\n  }\n}\n\nreturn {\n  json: {\n    ...(items[0].json),\n    avatarVideoUrl: avatarVideoUrl,\n    brollVideoUrl: brollVideoUrl,\n    videosReady: true\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "download-avatar-video",
      "name": "Download Avatar Video",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        4650,
        100
      ],
      "parameters": {
        "url": "={{ $json.avatarVideoUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "download-broll-video",
      "name": "Download B-Roll Video",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        4650,
        300
      ],
      "parameters": {
        "url": "={{ $json.brollVideoUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "upload-avatar-gdrive",
      "name": "Upload Avatar to GDrive",
      "type": "n8n-nodes-base.googleDrive",
      "onError": "continueRegularOutput",
      "position": [
        4850,
        100
      ],
      "parameters": {
        "name": "={{ 'avatar_' + $json.currentScript.avatarName + '_' + $json.scriptIndex + '.mp4' }}",
        "options": {
          "parentId": {
            "__rl": true,
            "mode": "id",
            "value": "={{ $json.rootFolderId }}"
          }
        },
        "operation": "upload",
        "resolveData": false
      },
      "typeVersion": 3
    },
    {
      "id": "upload-broll-gdrive",
      "name": "Upload B-Roll to GDrive",
      "type": "n8n-nodes-base.googleDrive",
      "onError": "continueRegularOutput",
      "position": [
        4850,
        300
      ],
      "parameters": {
        "name": "={{ 'broll_' + $json.scriptIndex + '.mp4' }}",
        "options": {
          "parentId": {
            "__rl": true,
            "mode": "id",
            "value": "={{ $json.rootFolderId }}"
          }
        },
        "operation": "upload",
        "resolveData": false
      },
      "typeVersion": 3
    },
    {
      "id": "merge-uploads",
      "name": "Merge Uploads",
      "type": "n8n-nodes-base.merge",
      "position": [
        5050,
        200
      ],
      "parameters": {
        "mode": "append",
        "outputDataFrom": "both"
      },
      "typeVersion": 3.2
    },
    {
      "id": "mark-saved",
      "name": "Mark Videos Saved",
      "type": "n8n-nodes-base.set",
      "position": [
        5250,
        200
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "video-saved",
              "name": "videosSaved",
              "type": "boolean",
              "value": "=true"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "retry-check",
      "name": "Retry Check",
      "type": "n8n-nodes-base.if",
      "position": [
        4450,
        400
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "combineOperation": "any"
          },
          "conditions": [
            {
              "id": "retry-condition",
              "operator": {
                "type": "boolean",
                "operation": "false"
              },
              "leftValue": "={{ $json.videosReady }}",
              "rightValue": "false"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "aggregate-results",
      "name": "Aggregate Results",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        5450,
        200
      ],
      "parameters": {
        "jsCode": "// Aggregate all generated assets\nconst allItems = $input.all();\nconst videoAssets = [];\n\nfor (const item of allItems) {\n  if (item.json.videosSaved) {\n    videoAssets.push({\n      segment: item.json.currentSegment ? item.json.currentSegment.segmentName : 'N/A',\n      avatar: item.json.currentScript ? item.json.currentScript.avatarName : 'N/A',\n      scriptTitle: item.json.currentScript ? item.json.currentScript.scriptTitle : 'N/A',\n      avatarVideoUrl: item.json.avatarVideoUrl,\n      brollVideoUrl: item.json.brollVideoUrl\n    });\n  }\n}\n\nreturn {\n  json: {\n    workflowId: allItems[0].json.workflowId,\n    productName: allItems[0].json.productName,\n    rootFolderUrl: allItems[0].json.rootFolderUrl,\n    totalAssets: videoAssets.length,\n    videoAssets: videoAssets,\n    completedAt: new Date().toISOString(),\n    status: 'completed'\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "notify-success",
      "name": "Notify Success (Slack)",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        5650,
        100
      ],
      "parameters": {
        "text": "=✅ UGC Video Generation Complete!\n\n*Workflow ID:* {{ $json.workflowId }}\n*Product:* {{ $json.productName }}\n*Total Assets:* {{ $json.totalAssets }}\n*Google Drive:* {{ $json.rootFolderUrl }}\n*Completed:* {{ $json.completedAt }}\n\n🎥 All videos have been organized in Google Drive!",
        "select": "channel",
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SLACK_CHANNEL_ID }}"
        },
        "operation": "post",
        "otherOptions": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "email-notification",
      "name": "Email Notification",
      "type": "n8n-nodes-base.emailSend",
      "onError": "continueRegularOutput",
      "position": [
        5650,
        200
      ],
      "parameters": {
        "message": "={{ '<h2>Your UGC Video Assets are Ready!</h2><p><strong>Workflow ID:</strong> ' + $json.workflowId + '</p><p><strong>Product:</strong> ' + $json.productName + '</p><p><strong>Total Assets Generated:</strong> ' + $json.totalAssets + '</p><p><strong>Google Drive Folder:</strong> <a href=\"' + $json.rootFolderUrl + '\">View Files</a></p><p>Completed at: ' + $json.completedAt + '</p>' }}",
        "options": {},
        "subject": "={{ 'UGC Video Assets Ready - ' + $json.productName }}",
        "toEmail": "={{ $env.NOTIFICATION_EMAIL_TO }}",
        "emailType": "html",
        "fromEmail": "={{ $env.NOTIFICATION_EMAIL_FROM }}",
        "operation": "send"
      },
      "typeVersion": 2.1
    },
    {
      "id": "respond-success",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        5850,
        200
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={{ { success: true, workflowId: $json.workflowId, status: $json.status, totalAssets: $json.totalAssets, googleDriveUrl: $json.rootFolderUrl, message: 'UGC video generation completed successfully' } }}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "handle-error",
      "name": "Handle Error",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        450,
        600
      ],
      "parameters": {
        "jsCode": "// Log error details\nconst error = $input.item.json.error || $input.item.json;\nconst errorMessage = error.message || JSON.stringify(error);\n\nconsole.error('Workflow Error:', errorMessage);\n\nreturn {\n  json: {\n    workflowId: $input.item.json.workflowId || 'unknown',\n    productName: $input.item.json.productName || 'unknown',\n    error: errorMessage,\n    failedAt: new Date().toISOString(),\n    status: 'failed'\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "notify-error",
      "name": "Notify Error (Slack)",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        650,
        600
      ],
      "parameters": {
        "text": "=❌ UGC Video Generation Failed\n\n*Workflow ID:* {{ $json.workflowId }}\n*Product:* {{ $json.productName }}\n*Error:* {{ $json.error }}\n*Failed at:* {{ $json.failedAt }}\n\nPlease check the workflow logs for details.",
        "select": "channel",
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SLACK_CHANNEL_ID }}"
        },
        "operation": "post",
        "otherOptions": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "respond-error",
      "name": "Respond Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        850,
        600
      ],
      "parameters": {
        "options": {
          "responseCode": 500
        },
        "respondWith": "json",
        "responseBody": "={{ { success: false, workflowId: $json.workflowId, status: 'failed', error: $json.error, message: 'UGC video generation failed' } }}"
      },
      "typeVersion": 1.4
    }
  ],
  "pinData": {},
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "errorWorkflow": "",
    "executionOrder": "v1",
    "saveManualExecutions": true
  },
  "versionId": "1",
  "connections": {
    "Retry Check": {
      "main": [
        [
          {
            "node": "Wait for Processing",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle Error": {
      "main": [
        [
          {
            "node": "Notify Error (Slack)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Uploads": {
      "main": [
        [
          {
            "node": "Mark Videos Saved",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Avatars": {
      "main": [
        [
          {
            "node": "Generate Scripts (Claude)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Scripts": {
      "main": [
        [
          {
            "node": "Split Scripts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Scripts": {
      "main": [
        [],
        [
          {
            "node": "Extract Current Script",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Split Segments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Job Info": {
      "main": [
        [
          {
            "node": "Wait for Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Segments": {
      "main": [
        [
          {
            "node": "Split Segments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Segments": {
      "main": [
        [],
        [
          {
            "node": "Extract Current Segment",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aggregate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Input": {
      "main": [
        [
          {
            "node": "Notify Start (Slack)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Create Root Folder (GDrive)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Folder Info": {
      "main": [
        [
          {
            "node": "Generate Audience Segments (Claude)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Validate Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Results": {
      "main": [
        [
          {
            "node": "Notify Success (Slack)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if Complete": {
      "main": [
        [
          {
            "node": "Extract Video URLs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Retry Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Videos Saved": {
      "main": [
        [
          {
            "node": "Split Scripts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Notification": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Video URLs": {
      "main": [
        [
          {
            "node": "Download Avatar Video",
            "type": "main",
            "index": 0
          },
          {
            "node": "Download B-Roll Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Avatar Status": {
      "main": [
        [
          {
            "node": "Merge Status Checks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check B-Roll Status": {
      "main": [
        [
          {
            "node": "Merge Status Checks",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Status Checks": {
      "main": [
        [
          {
            "node": "Check if Complete",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Avatar Job Info": {
      "main": [
        [
          {
            "node": "Merge Job Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set B-Roll Job Info": {
      "main": [
        [
          {
            "node": "Merge Job Info",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Wait for Processing": {
      "main": [
        [
          {
            "node": "Check Avatar Status",
            "type": "main",
            "index": 0
          },
          {
            "node": "Check B-Roll Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify Error (Slack)": {
      "main": [
        [
          {
            "node": "Respond Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Avatar Video": {
      "main": [
        [
          {
            "node": "Upload Avatar to GDrive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download B-Roll Video": {
      "main": [
        [
          {
            "node": "Upload B-Roll to GDrive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Current Script": {
      "main": [
        [
          {
            "node": "Generate Avatar Video (Nano Banana)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate B-Roll (Veo 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Current Segment": {
      "main": [
        [
          {
            "node": "Generate Avatars (Gemini)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate B-Roll (Veo 3)": {
      "main": [
        [
          {
            "node": "Set B-Roll Job Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Avatar to GDrive": {
      "main": [
        [
          {
            "node": "Merge Uploads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload B-Roll to GDrive": {
      "main": [
        [
          {
            "node": "Merge Uploads",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Generate Avatars (Gemini)": {
      "main": [
        [
          {
            "node": "Parse Avatars",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Scripts (Claude)": {
      "main": [
        [
          {
            "node": "Parse Scripts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Root Folder (GDrive)": {
      "main": [
        [
          {
            "node": "Set Folder Info",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Audience Segments (Claude)": {
      "main": [
        [
          {
            "node": "Parse Segments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Avatar Video (Nano Banana)": {
      "main": [
        [
          {
            "node": "Set Avatar Job Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}