Video content distribution is one of the highest-ROI automation targets for any team publishing regularly. The manual process — download, clip, caption, schedule, repeat across platforms — is genuinely painful at scale, and the automated version is achievable with tools most teams already have.
This post covers the full technical architecture of a content distribution pipeline built in n8n, using Dropbox as the entry point, Claude for AI copywriting, and Opus Clip for short-form video generation. Includes parallel processing architecture, Slack approval gate, error handling per failure type, and a free downloadable workflow JSON at the end.
The Problem This Solves
For a single long-form video, the manual distribution process:
- Download raw recording from storage
- Scrub through timeline to find highlight moments
- Clip each moment, export per platform aspect ratio
- Write separate captions for LinkedIn, Instagram, X, YouTube
- Schedule across platforms individually
For one video: 4–6 hours of manual work. For a team publishing 4+ videos a week: a part-time job with no clear owner, and the first thing to slip when the team gets busy.
The workflow below collapses all of that into one upload and one review.
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ n8n Content Distribution Workflow │
│ │
│ [Dropbox Trigger] ← watches /content-inbox/ on file creation │
│ ↓ │
│ [Dropbox: Download file binary] │
│ ↓ │
│ [Transcription Service] → raw transcript text │
│ ↓ │
│ ┌────────────────────────────────────────┐ │
│ │ PARALLEL BRANCHES │ │
│ │ │ │
│ │ [Claude API] [Opus Clip API] │ │
│ │ ↓ ↓ │ │
│ │ LinkedIn caption Short clips │ │
│ │ Instagram caption Auto-captions │ │
│ │ X thread Aspect ratios │ │
│ │ YouTube description │ │
│ │ 3 title variants │ │
│ └──────────────┬─────────────┬───────────┘ │
│ ↓ ↓ │
│ [Merge Node: Wait for both branches] │
│ ↓ │
│ [Slack: Send to #content-review] │
│ ↓ ↓ │
│ [Approve] [Reject] │
│ ↓ ↓ │
│ [Distribution] [Flag for edit] │
│ ↓ │
│ [Google Sheets: Log run] │
│ ↓ │
│ [Error Branch: Log + Alert] │
└──────────────────────────────────────────────────────────────────┘
Stack
| Tool | Role | Required? |
|---|---|---|
| n8n (self-hosted or Cloud) | Orchestration | Yes |
| Dropbox + API access | File trigger + storage | Yes |
| Transcription service | Audio → text | Yes |
| Claude API (Anthropic) | AI copywriting | Yes |
| Opus Clip | Short-form video clipping | Yes |
| Slack Bot | Approval gate + review | Recommended |
| Google Sheets | Run logging | Optional |
Node-by-Node Breakdown
1. Dropbox Trigger
Configure the n8n Dropbox node to watch one specific folder, not the whole account.
Folder path: /content-inbox/
Event type: File Created (NOT File Modified)
Why file creation only? Re-saving or renaming an existing file inside the watched folder must not re-trigger the workflow. Creation events only prevents duplicate processing.
Recommended folder structure:
/content-inbox/
/2026-09-10/
webinar-product-demo.mp4
/2026-09-12/
podcast-ep-47.mp4
Date-based subfolders keep uploads organized and make it easy to trace which workflow run produced which output.
Opus Clip format note: Standardise on one video export format before uploads start. A format mismatch (webm instead of mp4, for example) discovered mid-run is harder to debug than a pre-upload naming convention enforced at the source.
2. Download File from Dropbox
After the trigger fires, use a second Dropbox node to download the actual file binary:
// Dropbox node config
Operation: Download File
File Path: {{ $json.path_display }}
Binary Property: videoFile
Pass the binary to the transcription node in the next step.
3. Transcription
Send the video binary (or a Dropbox shared URL) to your transcription service.
Critical: Transcription accuracy is the foundation of everything downstream. Claude works from text — it has no access to the original audio. A garbled product name or misheared technical term in the transcript appears in every caption generated from it.
Test the transcription service on several different recording types before going live:
- Different speakers and accents
- Variable audio quality (room acoustics, microphone quality)
- Technical vocabulary specific to your industry
Store the clean transcript as {{ $json.transcript }} for the next stage.
4. Parallel Branch: Claude Copywriting
Start the Claude branch and the Opus Clip branch simultaneously using n8n's parallel execution. Do not run them sequentially — Claude + Opus Clip in parallel is what gets turnaround from days to hours.
Brand Brief Structure
Claude has no memory between API calls. Every request must include the brand brief fresh:
You are a content writer for [Brand Name].
Brand brief:
- Audience: [description of target reader/viewer]
- Tone: [conversational / thought-leadership / technical / warm]
- Voice: [active sentences, avoid passive voice, no corporate jargon]
- Vocabulary to avoid: [list specific phrases]
- Example sentences that match our voice:
"[Example 1]"
"[Example 2]"
"[Example 3]"
Using the video transcript below, generate the following:
1. LinkedIn post (hook + 3-4 body lines + CTA, under 300 words)
2. Instagram caption (conversational, 5-7 hashtags at end)
3. X thread (4-5 tweets, each under 280 chars, numbered)
4. YouTube description (keyword-rich, 150-200 words, include timestamps if available)
5. Three title variants for the video (different angles, under 70 chars each)
Transcript:
{{ $json.transcript }}
Return ONLY valid JSON — no preamble, no markdown fences:
{
"linkedin": "...",
"instagram": "...",
"x_thread": ["tweet 1", "tweet 2", "tweet 3", "tweet 4"],
"youtube_description": "...",
"titles": ["title 1", "title 2", "title 3"]
}
The brand brief is not optional. Without it, Claude produces output that is technically correct but generic — immediately recognisable as AI-generated. The brief is the difference between copy that reads like the brand and copy that reads like a template.
Parse Claude Response
// Function Node — parse Claude JSON
const raw = $input.first().json.content[0].text;
// Strip accidental markdown fences
const clean = raw
.replace(/^```
{% endraw %}
json\s*/i, '')
.replace(/\s*
{% raw %}
```$/,'')
.trim();
let parsed;
try {
parsed = JSON.parse(clean);
} catch(e) {
throw new Error(`Claude JSON parse failed. Raw: ${raw.substring(0, 300)}`);
}
return [{
json: {
linkedin: parsed.linkedin,
instagram: parsed.instagram,
x_thread: parsed.x_thread,
youtube: parsed.youtube_description,
titles: parsed.titles
}
}];
5. Parallel Branch: Opus Clip
While Claude generates copy, route the video to Opus Clip simultaneously.
Opus Clip identifies clip-worthy moments automatically — no manual scrubbing — and returns:
- Short clips (typically 30–90 seconds)
- Auto-generated captions per clip
- Multiple aspect ratios (9:16 vertical, 1:1 square)
Store returned clip URLs as an array for the merge step.
Rate limit note: Opus Clip's processing queue can back up during burst uploads (multiple videos submitted simultaneously at the start of a week). Build in a queue delay if processing multiple videos in one batch run.
6. Merge Node
After both branches complete, use n8n's Merge node to combine outputs before routing to review.
Merge mode: Wait for All Inputs
Input 1: Claude copy branch
Input 2: Opus Clip clips branch
Output: {
copy: { linkedin, instagram, x_thread, youtube, titles },
clips: [ clip_url_1, clip_url_2, clip_url_3 ]
}
The merge node pauses until both branches return results. If either branch fails, the error branch triggers instead of the merge proceeding with incomplete data.
7. Slack Approval Gate
Do not skip this step. Everything goes through human review before any content reaches a public channel.
// Build Slack Block Kit review message
const { linkedin, titles, clips } = mergedOutput;
const blocks = [
{
type: "header",
text: { type: "plain_text", text: "📹 New Content Ready for Review" }
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Source file:* ${sourceFileName}\n*Titles generated:*\n• ${titles.join('\n• ')}`
}
},
{
type: "section",
text: { type: "mrkdwn", text: `*LinkedIn draft:*\n${linkedin.substring(0, 300)}...` }
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Clips generated:* ${clips.length}\n${clips.map((url, i) => `<${url}|Clip ${i+1}>`).join(' · ')}`
}
},
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "✅ Approve & Schedule" },
style: "primary",
value: "approve",
action_id: "content_approve"
},
{
type: "button",
text: { type: "plain_text", text: "❌ Reject" },
style: "danger",
value: "reject",
action_id: "content_reject"
}
]
}
];
Approval response handling:
- Slack sends a POST to a second n8n webhook when the button is clicked
- n8n's Wait node pauses the workflow until that webhook fires
-
approve→ continue to distribution -
reject→ flag source file for manual editing, stop workflow, log outcome
For regulated industries (healthcare, finance, legal): Add a second approval gate for compliance review before the first distribution step. Log approver identity and timestamp on every run.
8. Error Handling
Every external API node needs an error branch. Build one that covers the full pipeline:
// Error branch function node
const errorDetails = {
timestamp: new Date().toISOString(),
pipeline: "Content Distribution",
failed_node: $input.first().json.failedNode || "unknown",
error_message: $input.first().json.error?.message || "Unknown error",
source_file: $('Dropbox Trigger').first().json?.name || "unknown",
status_code: $input.first().json.error?.statusCode
};
return [{ json: errorDetails }];
After capturing error details:
- Log to Google Sheets (
source_file,failed_node,error_message,timestamp) - Send Slack alert to
#content-opswith error summary - Do NOT retry automatically for format errors or auth failures
Common Failure Points
| Failure | Cause | Handling |
|---|---|---|
| Transcription timeout | Long video (60+ min), slow service | Retry once (60s delay), then alert |
| Claude empty response | Prompt too long, API overload | Retry once, then flag for manual |
| Claude JSON parse error | Model added markdown fences | Strip with regex, retry once |
| Opus Clip content rejection | Policy violation on clip content | Flag for manual review, do not retry |
| Dropbox 401 | OAuth token expired | Alert team, manual token refresh required |
| Claude/Opus rate limit (429) | Burst of uploads | Queue with delay, retry after backoff |
| Merge timeout | One branch never returned | Alert, log which branch failed |
Customisation Options
Swap Dropbox for Google Drive
If the team stores recordings in Google Drive instead of Dropbox, replace the trigger and download nodes with their Google Drive equivalents. The rest of the workflow is unchanged. Test the file binary format — Google Drive exports may need format conversion before transcription.
Add a Video Length Filter
Skip processing for videos under a threshold (e.g. under 5 minutes rarely have enough content to clip):
// IF node condition
{{ $json.duration_seconds }} > 300
// true → continue
// false → log as skipped, notify uploader
Route by File Naming Convention
Apply different processing rules based on file name:
const filename = $json.name.toLowerCase();
if (filename.includes('internal')) {
return [{ json: { route: 'skip_distribution' } }];
} else if (filename.includes('client-')) {
return [{ json: { route: 'client_approval_chain' } }];
} else {
return [{ json: { route: 'standard' } }];
}
Multi-Client / Multi-Brand Setup
Run separate workflow instances per client, each with:
- A dedicated Dropbox folder
- A client-specific brand brief in the Claude node
- A separate Slack channel for review
- Individual Google Sheets log per client
Avoid conditional branching inside one workflow for multi-client setups — separate instances are easier to debug, permission, and hand off.
Add Google Sheets Logging
After every successful run (approved or rejected), log to a sheet:
Columns: source_file | run_date | clips_generated | approved_by | approved_at | platforms_scheduled | status
This creates a searchable content record without maintaining it manually.
Limitations
Transcription quality gates everything. Poor audio = poor transcript = poor AI copy. No amount of prompt tuning compensates for a bad transcript. Improve recording quality at the source before optimising the pipeline.
Claude can't evaluate factual accuracy. It generates from the transcript. If the speaker said something imprecise, the caption reflects it.
Opus Clip selects candidates, not guarantees. Some clips will miss context obvious to a human reviewer. The approval gate catches these.
No persistent dedup by default. If the same file is uploaded twice (re-saved to the same folder), the workflow fires twice. Add a filename-based check in a Function node before the transcription step if this is a concern.
API rate limits at scale. Claude API and Opus Clip processing queues can throttle under burst load. For high-volume setups (multiple videos per day), stagger uploads or add queue delays between workflow runs.
Get the Free Workflow JSON
IT Path Solutions published the complete n8n workflow — Dropbox trigger, transcription node, parallel Claude + Opus Clip branches, merge node, Slack approval gate, error handling branch, and Google Sheets logging — all pre-connected.
Import into any n8n instance (self-hosted or Cloud), add credentials, run against one video first.
👉 Download the free workflow JSON + full setup guide
Setup guide covers: Dropbox API setup, Claude API key configuration, Opus Clip account connection, Slack bot token scoping, brand brief structure, and how to test end-to-end before going live.
Summary
The pipeline works because it runs Claude and Opus Clip in parallel, routes everything through a single human review gate, and logs every run automatically. A 45-minute recording becomes a full set of clips and platform-specific copy in a few hours — ready to approve, not ready to rework from scratch.
Run it against one video. Review the output. Adjust the brand brief if the copy tone is off. Then decide if it's ready for the full pipeline.
Full guide and JSON: itpathsolutions.com/automated-content-distribution-pipeline
What breaks first in your content distribution setup? Drop it in the comments.
Top comments (0)