This guide walks you through building the workflow manually in n8n, which gives you better control and understanding than importing JSON.
- ✅ n8n running (you have this!)
- ✅ OpenAI API key
- ✅ yt-dlp installed on server
- ✅ FFmpeg installed on server
- Add new workflow in n8n
- Add node → Search "Webhook"
- Configure:
- HTTP Method:
POST - Path:
instagram-gist - Response Mode:
Using 'Respond to Webhook' Node
- HTTP Method:
- Save the node
Your webhook URL will be: http://your-n8n-domain/webhook/instagram-gist
- Add node → Search "IF"
- Connect from Webhook node
- Configure:
- Condition 1:
- Value 1:
{{ $json.body.url }} - Operation:
contains - Value 2:
instagram.com
- Value 1:
- Condition 1:
- Save
This creates two paths: TRUE (valid) and FALSE (invalid)
- Add node → Search "Respond to Webhook"
- Connect from IF node's FALSE output
- Configure:
- Respond With:
JSON - JSON Response:
{ "error": "Invalid Instagram URL. Please provide a valid Instagram reel URL." }
- Respond With:
- Save
- Add node → Search "Execute Command"
- Connect from IF node's TRUE output
- Configure:
- Command:
yt-dlp -f "best[ext=mp4]/best" --no-playlist -o "/tmp/insta-gist-{{ $json.body.url.match(/\/(?:reel|p)\/([A-Za-z0-9_-]+)/)[1] }}.mp4" "{{ $json.body.url }}"
- Command:
- Save
Note: The regex extracts the video ID (e.g., DOgC-ZjiEF3) from the URL and downloads to /tmp/. This works with query parameters like ?igsh=...
- Add node → Search "Set"
- Connect from Execute Command
- Configure - Add these fields:
- videoId:
- Type:
String - Value:
{{ $json.body.url.match(/\/(?:reel|p)\/([A-Za-z0-9_-]+)/)[1] }}
- Type:
- videoPath:
- Type:
String - Value:
/tmp/insta-gist-{{ $json.body.url.match(/\/(?:reel|p)\/([A-Za-z0-9_-]+)/)[1] }}.mp4
- Type:
- audioPath:
- Type:
String - Value:
/tmp/insta-gist-{{ $json.body.url.match(/\/(?:reel|p)\/([A-Za-z0-9_-]+)/)[1] }}.mp3
- Type:
- originalUrl:
- Type:
String - Value:
{{ $json.body.url }}
- Type:
- videoId:
- Save
Note: The regex expression match(/\/(?:reel|p)\/([A-Za-z0-9_-]+)/) correctly extracts the video ID from Instagram URLs regardless of query parameters or format variations.
- Add node → Search "Execute Command"
- Connect from Set node
- Configure:
- Command:
ffmpeg -i {{ $json.videoPath }} -vn -acodec libmp3lame -ac 1 -ar 16000 -ab 128k {{ $json.audioPath }} -y
- Command:
- Save
What this does: Extracts audio, converts to mono 16kHz MP3 (optimized for Whisper)
- Add node → Search "Read Binary File"
- Connect from previous Execute Command
- Configure:
- File Path:
{{ $json.audioPath }} - Property Name:
audioFile
- File Path:
- Save
- Add node → Search "HTTP Request"
- Connect from Read Binary File
- Configure:
- Authentication: Select your OpenAI credential
- Method:
POST - URL:
https://api.openai.com/v1/audio/transcriptions - Send Body:
Yes - Body Content Type:
Form-Data Multipart - Add Parameter:
- Name:
file - Input Data Field Name:
audioFile
- Name:
- Add Parameter:
- Name:
model - Value:
whisper-1
- Name:
- Add Parameter (optional):
- Name:
language - Value:
en
- Name:
- Save
- Add node → Search "HTTP Request"
- Connect from previous HTTP Request
- Configure:
- Authentication: Select your OpenAI credential
- Method:
POST - URL:
https://api.openai.com/v1/chat/completions - Send Body:
Yes - Body Content Type:
JSON - JSON/RAW Parameters:
{ "model": "gpt-4o-mini", "messages": [ { "role": "system", "content": "You are a helpful assistant that creates concise summaries of video transcripts. Create a summary with 3-5 bullet points highlighting the key messages, main topics, and actionable insights." }, { "role": "user", "content": "Summarize the following video transcript:\n\n{{ $json.text }}" } ], "temperature": 0.7, "max_tokens": 500 }
- Save
Note: Using gpt-4o-mini instead of gpt-4 saves cost (~90% cheaper)
- Add node → Search "Set"
- Connect from previous HTTP Request
- Configure - Add these fields:
- summary:
- Type:
String - Value:
{{ $json.choices[0].message.content }}
- Type:
- transcript:
- Type:
String - Value:
{{ $node["HTTP Request"].json.text }}
- Type:
- videoId:
- Type:
String - Value:
{{ $node["Set"].json.videoId }}
- Type:
- originalUrl:
- Type:
String - Value:
{{ $node["Set"].json.originalUrl }}
- Type:
- summary:
- Save
- Add node → Search "Execute Command"
- Connect from Set node
- Configure:
- Command:
rm -f {{ $node["Set"].json.videoPath }} {{ $node["Set"].json.audioPath }}
- Command:
- Save
- Add node → Search "Respond to Webhook"
- Connect from Execute Command
- Configure:
- Respond With:
JSON - Response Data Source:
Define Below for Each Property - Add all the fields from the previous Set node
- Respond With:
- Save
Click the Active toggle in the top right
curl -X POST http://your-n8n-domain/webhook/instagram-gist \
-H "Content-Type: application/json" \
-d '{"url": "https://www.instagram.com/reel/DOgC-ZjiEF3/"}'export WEBHOOK_URL="http://your-n8n-domain/webhook/instagram-gist"
./scripts/test-webhook.sh "https://www.instagram.com/reel/DOgC-ZjiEF3/"Webhook (POST)
↓
IF (validate URL)
├─ TRUE → Download Video (yt-dlp)
│ ↓
│ Set Variables
│ ↓
│ Extract Audio (ffmpeg)
│ ↓
│ Read Audio File
│ ↓
│ Transcribe (Whisper API)
│ ↓
│ Summarize (GPT-4)
│ ↓
│ Format Response
│ ↓
│ Cleanup Files
│ ↓
│ Respond with Success
│
└─ FALSE → Error Response
Solution: yt-dlp is not installed on your n8n server. For Render.com, you need to use the custom Docker image.
# Build and deploy custom Docker image
./scripts/build-and-push.sh your-dockerhub-usernameSolution: Same as above - use the custom Docker image with FFmpeg included.
Possible causes:
- Audio file is too large (>25MB limit)
- Audio file format not supported
- File path is wrong
Solution:
- Check the audio file was created: Add a debug Execute Command node with
ls -lh /tmp/insta-gist-* - Verify the Read Binary File node is reading the correct path
Possible causes:
- Video is from a private account
- Video was deleted
- Instagram blocking the request
Solution:
- Test with a different public Instagram reel
- Add user-agent to yt-dlp command:
yt-dlp --user-agent "Mozilla/5.0" -f "best[ext=mp4]/best" ...
Possible causes:
- Long video (>5 minutes)
- Slow network
- API rate limiting
Solution:
- Set execution timeout in n8n settings (Settings → Execution Timeout)
- Consider adding a queue for long-running jobs
In Step 9, change the model from gpt-4 to gpt-4o-mini:
- Cost: ~$0.15 per 1M input tokens (vs $5 for GPT-4)
- Quality: Still excellent for summaries
- Speed: Faster responses
To avoid reprocessing the same video:
- Add a "Check Cache" node before download (e.g., check Redis or database)
- If cached, return cached summary
- If not cached, process and save to cache
For videos >10 minutes:
- Add a check for video duration before processing
- Return an error or queue for async processing
- Consider chunking audio for Whisper (it has a 25MB limit)
- ✅ Build the workflow following these steps
- ✅ Test with the Instagram URL from your test data
- ✅ Add error handling for each node
- ✅ Set up monitoring/alerting for failures
- ✅ Consider adding a database for caching results
For production, add error handling to each node:
- Click on a node
- Settings → On Error
- Choose "Continue" to prevent workflow from stopping
- Add an error notification node (email, Slack, etc.)
If you encounter issues:
- Check n8n execution logs (Executions tab)
- Test each node individually using the "Test" button
- Verify credentials are correctly linked
- Check server logs for yt-dlp and ffmpeg errors