.claude/skills/instagram-reels/SKILL.md
Produce an Instagram Reel for Clarido — remix an existing TikTok concept with a deeper script, new voiceover, and expanded illustrations, then publish via the Instagram API. Use when creating new Instagram Reels content.
npx skillsauth add Abhi5415/clarido-marketing instagram-reelsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
End-to-end workflow for producing and publishing an Instagram Reel. These are not cross-posts — they're remixed versions of TikTok concepts: same psychology insight, but a deeper/longer script tailored for Instagram's audience, new voiceover, and additional illustrations to support the expanded content.
CRITICAL: Do NOT publish to Instagram without explicit user approval of the rendered video. Show them the video file path and wait for "go" before uploading/publishing.
Before creating ANY new reel, analyze all prior reels to understand what's working, form a testable hypothesis, and structure the new content as an experiment.
Use the Meta Graph API to fetch performance data for every published reel. The Instagram Business Account ID is 17841477134994858.
Get all media IDs:
pipenv run python3 -c "
import requests, os
from dotenv import load_dotenv
load_dotenv()
token = os.getenv('META_ACCESS_TOKEN')
account_id = '17841477134994858'
url = f'https://graph.facebook.com/v21.0/{account_id}/media'
params = {'fields': 'id,caption,timestamp,media_type,media_url,permalink', 'access_token': token, 'limit': 50}
r = requests.get(url, params=params)
data = r.json()
for m in data.get('data', []):
if m.get('media_type') == 'VIDEO':
print(f\"{m['id']} | {m['timestamp'][:10]} | {m.get('permalink','')} | {m.get('caption','')[:60]}\")
"
For each reel, fetch insights:
# Key metrics for each media_id:
pipenv run python3 -c "
import requests, os
from dotenv import load_dotenv
load_dotenv()
token = os.getenv('META_ACCESS_TOKEN')
media_id = 'MEDIA_ID_HERE'
url = f'https://graph.facebook.com/v21.0/{media_id}/insights'
params = {
'metric': 'ig_reels_avg_watch_time,ig_reels_video_view_total_time,reach,views,likes,saved,shares,comments,total_interactions',
'access_token': token
}
r = requests.get(url, params=params)
for m in r.json().get('data', []):
print(f\"{m['name']}: {m['values'][0]['value']}\")
"
Get video duration via ffprobe (the IG API does not return video_duration reliably):
ffprobe -v error -show_entries format=duration -of csv=p=0 "MEDIA_URL_HERE"
Some reels may be boosted with ad spend. Pull all active and recent Instagram Reel promotion campaigns from the Meta Marketing API to understand which reels have paid reach inflating their numbers.
Get all ads promoting Instagram Reels:
pipenv run python3 -c "
import requests, os, json
from dotenv import load_dotenv
load_dotenv()
token = os.getenv('META_ACCESS_TOKEN')
ad_account = os.getenv('META_AD_ACCOUNT_ID') # act_217614761779242
# Get all campaigns (active + paused + completed)
url = f'https://graph.facebook.com/v21.0/{ad_account}/campaigns'
params = {
'fields': 'id,name,status,objective',
'filtering': json.dumps([{'field': 'name', 'operator': 'CONTAIN', 'value': 'Instagram Reel'}]),
'access_token': token,
'limit': 50
}
r = requests.get(url, params=params)
campaigns = r.json().get('data', [])
for c in campaigns:
print(f\"Campaign: {c['name']} | Status: {c['status']} | ID: {c['id']}\")
# Get adsets under this campaign
adsets_url = f'https://graph.facebook.com/v21.0/{c[\"id\"]}/adsets'
adsets_params = {'fields': 'id,name,daily_budget,status', 'access_token': token}
adsets = requests.get(adsets_url, params=adsets_params).json().get('data', [])
for a in adsets:
budget = int(a.get('daily_budget', 0)) / 100
print(f' Adset: {a[\"name\"]} | \${budget:.0f}/day | Status: {a[\"status\"]}')
# Get ads + spend/reach
ads_url = f'https://graph.facebook.com/v21.0/{a[\"id\"]}/ads'
ads_params = {'fields': 'id,name,status,insights{spend,reach,impressions,actions}', 'access_token': token}
ads = requests.get(ads_url, params=ads_params).json().get('data', [])
for ad in ads:
insights = ad.get('insights', {}).get('data', [{}])[0]
spend = insights.get('spend', '0')
reach = insights.get('reach', '0')
print(f' Ad: {ad[\"name\"]} | Spend: \${spend} | Paid Reach: {reach} | Status: {ad[\"status\"]}')
print()
"
Why this matters: A reel's organic reach/saves numbers may be inflated by paid promotion. When comparing reels, note which ones had paid support and separate organic vs paid performance. A reel with 2,000 reach and $0 spend is very different from one with 2,000 reach and $50 spend.
What to include in the performance table: Add columns for Paid Spend and Paid Reach (or $0 / organic only for unboosted reels). When a reel is currently being promoted, note it as ACTIVE $X/day.
For each reel, compute: retention % = (ig_reels_avg_watch_time_ms / 1000) / video_duration_seconds * 100
Note on paid vs organic retention: The ig_reels_avg_watch_time metric from the IG API is a blend of organic and paid viewers. Paid viewers typically have lower retention (they didn't choose to see your content). If a reel is being promoted, its retention % may be dragged down by paid traffic. Keep this in mind when comparing promoted vs unpromoted reels.
For each reel, classify these attributes (read the corresponding brief.md and Remotion config.ts):
| Attribute | How to measure | Why it matters |
|-----------|---------------|----------------|
| Hook type | Read first slide text/voiceover. Classify as: identity/scenario, factoid/historical, contrarian, numbered-list, curiosity-gap | Identity hooks outperform factoid hooks 5-13x in our data |
| First slide duration | Read config.ts slide definitions, calculate first slide duration in seconds | First slides under 3s correlate with higher retention |
| Emotional valence | Read the hook. Classify as: neutral, negative, positive | Neutral valence hooks outperform negative valence hooks |
| Total slides | Count slide definitions in config.ts | More slides = more visual variety = more retention |
| Average pacing | video_duration / total_slides | 3.0-3.5s avg pacing correlates with best retention |
| Video duration | ffprobe measurement | Shorter videos may retain better (but sample size is small) |
| Has CTA | Check if voiceover mentions Clarido | Product CTAs may affect retention/saves |
Compile all metrics into a single comparison table:
| Reel | Date | Retention | Reach | Saves | Paid Spend | Paid Reach | Hook Type | Valence | 1st Slide | Slides | Pacing |
|------|------|-----------|-------|-------|------------|------------|-----------|---------|-----------|--------|--------|
Sort by retention (highest first). Flag any reel with active paid promotion — its reach and retention numbers are a blend of organic and paid traffic. This table is the foundation for hypothesis formation.
Based on the patterns in the data, form ONE specific, testable hypothesis for the next reel. Format:
Hypothesis: [If we do X], the new reel will achieve [specific metric target]. Based on: [which data points from the performance table support this]. Test: [what the new reel will do differently to test this]. Success criteria: retention > Y%, or saves > Z, or reach > W. Failure criteria: retention < A% indicates the hypothesis is wrong.
Example hypotheses (based on our Feb 2026 baseline data):
Output the performance table and hypothesis to the user BEFORE proceeding to concept selection. The user should see what the data says and what experiment we're running. This is informational only — don't wait for approval to continue.
After the reel is published, update the reel's performance.md with:
Reference data from prior analysis. Update this section as new reels are published and analyzed.
| Reel | Date | Retention | Reach | Saves | Paid | Hook Type | Valence | 1st Slide | Slides | Avg Pacing | |------|------|-----------|-------|-------|------|-----------|---------|-----------|--------|------------| | Attention Residue | Feb 27 | 79% | 74 | 1 | ACTIVE $5/day (since Mar 1) | identity/scenario | neutral | 2.56s | 23 | 3.4s | | Mere Exposure | Feb 21 | 16% | 103 | 0 | $4.77 spent (paused Mar 1) | factoid | negative | 2.56s | 20 | ~3.1s | | Zeigarnik Effect | Feb 28 | 13% | 124 | 0 | $0 organic only | factoid/historical | neutral | 5.68s | 18 | 3.7s | | Affect Labeling | Feb 22 | 10% | 58 | 0 | $0 organic only | factoid | neutral | 4.24s | 20 | ~3.9s | | Rumination | Feb 23 | 6% | 36 | 0 | $0 organic only | identity/scenario | negative | 2.88s | 20 | 4.0s |
Patterns identified:
The user may invoke this skill in three ways:
content/instagram-reels/*/brief.md to see what's already been remixed. Read ALL content/tiktok/*/brief.md to see available source concepts.Read the source TikTok brief.md to understand the concept, study, and original script.
Write a new Instagram script — not a copy of the TikTok script:
docs/writing-style-reference.md)Write Instagram caption — distinctly different from TikTok captions:
Write 3-5 hashtags — topic-categorization style, not search-keyword style:
#psychology #selfawareness #mentalhealth #overthinking #emotionalintelligence#fyp #viral #reels #explore (vanity tags)Agent review — Before presenting to the user, launch a separate Opus agent to proof the script. Use the Agent tool with model: "opus" and include in the prompt:
docs/writing-style-reference.md)Present revised script + caption + hashtags to user. Mention key changes from the agent review. Wait for approval.
After script approval, run these in sequence:
Create content folder: content/instagram-reels/YYYY-MM-DD_HHMM_slug/ and save brief.md.
Voiceover: pipenv run python3 scripts/tiktok/voiceover.py <content-folder>
Background music: Generate via ElevenLabs music API (POST /v1/music):
music_v1music_length_ms should be ~10s longer than voiceover duration (to cover the full video)force_instrumental: trueassets/background-music.mp3videos/public/{slug}-reel-music.mp3Transcription: pipenv run python3 scripts/tiktok/transcribe.py <content-folder>
Generate captions data — Convert the Deepgram transcript.json into a TypeScript file for the Remotion caption overlay:
assets/transcript.json, extract word-level timestampsvideos/src/{ConceptName}Reel/captions.ts exporting CAPTIONS: Caption[] (from @remotion/captions){ text, startMs, endMs, timestampMs, confidence }buildPhraseGroups() uses punctuation (., ,, !, ?, ;, :) to break word groups. Without punctuation, groups will be too long.Plan slides — Use transcript timestamps to plan slides at ~3s each:
Write illustrations JSON — Save to assets/illustrations.json (only new illustrations):
[{"name": "slug-name", "prompt": "Scene description (style prefix added automatically)"}]
Generate new illustrations: pipenv run python3 scripts/tiktok/illustrations.py <illustrations-json> <output-dir>
Prepare assets — Copy to videos/public/ for Remotion:
content/tiktok/.../assets/illustrations/content/instagram-reels/.../assets/illustrations/{slug}-reel-voiceover.mp3{slug}-reel-music.mp3Build Remotion composition at videos/src/{ConceptName}Reel/ with 4 files:
config.ts — FPS (30), duration frames, slide definitionsSlide.tsx — Individual slide component (same as TikTok pattern)CaptionOverlay.tsx — On-screen caption component (see Caption Spec below)captions.ts — Word-level timestamp data from Deepgramindex.tsx — Main composition combining slides + voiceover + music + captions<Folder name="Instagram"> sectionComposition structure (index.tsx):
<Audio> for voiceover at volume={1}<Audio> for background music at volume={0.12} (low, behind narration)<Sequence> for each slide synced to voiceover timestamps<CaptionOverlay /> layered on top of everythingRender: cd videos && npx remotion render CompositionId out/{slug}-reel.mp4
Archive: Copy mp4 to content/instagram-reels/YYYY-MM-DD_HHMM_slug/
Upload to Supabase: pipenv run python3 scripts/instagram/upload_to_supabase.py <video-file>
Post to Instagram: pipenv run python3 scripts/instagram/post_reel.py "<public-url>" "<caption + hashtags>"
Update brief.md with media ID, permalink, and publish timestamp.
Delete from Supabase — Clean up after confirmed publishing:
pipenv run python3 -c "import sys; sys.path.insert(0,'.'); from scripts.instagram.upload_to_supabase import delete_video; delete_video('filename.mp4')"
Email confirmation — Send to both [email protected] and [email protected] via mcp__google-workspace__send_gmail_message:
[email protected] as user_google_email){Concept Name} — Instagram Reel PublishedNot every video should pitch Clarido — but some must, otherwise the content builds an audience that doesn't know what we sell. Rotate across videos:
Keep it natural and earned — the viewer just learned something real, and the CTA connects the insight to the tool:
Never make the CTA the focus of the video. It's a 2-3 second tag at the end, not a pitch.
Instagram's algorithm heavily weights DM shares/sends. Every caption should include a send-oriented CTA:
Use save CTAs occasionally too ("save this for your next spiral"), but prioritize sends. Vary the CTA each video — don't repeat the same one back-to-back.
Instagram does NOT auto-generate on-screen captions for Reels uploaded via the API (only closed captions for accessibility). On-screen captions are critical for sound-off browsing, so we bake them into the video.
#B91C1C), rounded corners (borderRadius: 5)0px 4px) — only backgroundColor changes between transparent and #B91C1C. This prevents layout shift.flexWrap: "nowrap", whiteSpace: "nowrap"paddingBottom: 300 (above Instagram UI controls)Groups words into natural phrases using:
., ,, !, ?, ;, : (if group has ≥ 2 words)CaptionOverlay.tsx — Self-contained component, imports captions.tscaptions.ts — Generated from Deepgram transcript, exports Caption[] from @remotion/captionscreateTikTokStyleCaptions from @remotion/captions — it merges too many words into single pages with continuous speech. Use the custom buildPhraseGroups() function instead.videos/src/PlanningFallacyReel/CaptionOverlay.tsx"-2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000, -3px 0 0 #000, 3px 0 0 #000, 0 -3px 0 #000, 0 3px 0 #000"
Applied to ALL words at ALL times (including when the red background is active).
POST /v1/music, model music_v1)volume={0.12} in Remotion — barely audible, just adds atmosphere behind narrationforce_instrumental: true — always| Script | Purpose | Input | Output |
|--------|---------|-------|--------|
| scripts/tiktok/voiceover.py | ElevenLabs TTS + 1.25x speedup | Content folder (reads brief.md) | assets/voiceover-raw.mp3, assets/voiceover.mp3 |
| scripts/tiktok/transcribe.py | Deepgram Nova-3 word timestamps | Content folder (reads voiceover.mp3) | assets/transcript.json + printed table |
| scripts/tiktok/illustrations.py | OpenAI gpt-image-1.5 with style refs | JSON config + output dir | Portrait PNGs in output dir |
| scripts/tiktok/prepare_assets.py | Copy assets to Remotion public/ | Content folder | Files in videos/public/ |
| scripts/instagram/upload_to_supabase.py | Upload video to Supabase Storage | Video file path | Public URL (stdout) |
| scripts/instagram/post_reel.py | Publish Reel via Instagram API | Public video URL + caption | Media ID + permalink (stdout) |
content/instagram-reels/YYYY-MM-DD_HHMM_slug/
├── brief.md # Concept, script, caption, hashtags, slide plan, source ref, media ID
├── assets/
│ ├── illustrations/ # New illustrations only (reused ones referenced from TikTok folder)
│ ├── illustrations.json # Illustration specs for new illustrations
│ ├── background-music.mp3 # ElevenLabs generated ambient music
│ ├── voiceover-raw.mp3
│ ├── voiceover.mp3
│ └── transcript.json
├── slug-reel.mp4 # Final rendered video
└── performance.md # Experiment tracking + analytics (see template below)
videos/src/{ConceptName}Reel/
├── config.ts # FPS, duration, slide definitions
├── Slide.tsx # Slide component (illustration + fade/zoom)
├── CaptionOverlay.tsx # On-screen captions (word-by-word highlight)
├── captions.ts # Word timestamps from Deepgram (Caption[])
└── index.tsx # Main composition (slides + audio + music + captions)
Every reel's performance.md should follow this format for experiment tracking:
# Performance — {Reel Name}
## Experiment
**Hypothesis**: {What we're testing}
**Based on**: {Data points from Phase 0 analysis}
**Success criteria**: {Specific metric targets}
**Failure criteria**: {What would disprove the hypothesis}
## Content Attributes
| Attribute | Value |
|-----------|-------|
| Hook type | {identity/scenario, factoid, contrarian, etc.} |
| Emotional valence | {neutral, negative, positive} |
| First slide duration | {X.Xs} |
| Total slides | {N} |
| Average pacing | {X.Xs} |
| Video duration | {Xs} |
| Has CTA | {yes/no} |
## Metrics (Organic)
| Metric | 48h | 7d | 30d |
|--------|-----|-----|------|
| Retention % | | | |
| Avg watch time | | | |
| Reach | | | |
| Views | | | |
| Likes | | | |
| Saves | | | |
| Shares | | | |
| Comments | | | |
| New followers | | | |
## Paid Promotion (if applicable)
| Metric | Value |
|--------|-------|
| Campaign name | {name or "N/A — not promoted"} |
| Daily budget | |
| Total spend | |
| Paid reach | |
| Paid impressions | |
| ThruPlays | |
| Cost per ThruPlay | |
| Video watch 25%/50%/75%/100% | |
## Verdict
{After 7 days: confirmed / rejected / inconclusive}
{Brief explanation of what the data shows and what to test next}
createTikTokStyleCaptions — it produces giant pages with continuous speech. Use custom buildPhraseGroups().backgroundColor.flexWrap: "nowrap", max 4 words per group, AND max 20 characters per group. The character limit prevents long words (like "AUSTRALIAN GOVERNMENT") from overflowing 1080px. Both limits must be in buildPhraseGroups().volume={0.12}.1024x1536 (portrait), NOT 1024x1024<Audio> from @remotion/media, NOT from core remotionoverflow: "visible" on slide container to prevent zoom clippingperformance.md with the hypothesis and content attributes. Pull 48h metrics and 7-day metrics to validate.docs/writing-style-reference.mddocs/tiktok-psych-video-method.md (parent pattern)assets/illustrations/overwhelmed-filing-cabinet.png, floating-yellow-balloon.png, planting-yellow-seedling.pngvideos/src/MereExposureEffectReel/ (config.ts, Slide.tsx, CaptionOverlay.tsx, captions.ts, index.tsx)content/instagram-reels/2026-02-21_2020_mere-exposure-effect/brief.mdvideos/src/ParadoxOfChoiceTikTok/ (config.ts, Slide.tsx, index.tsx)development
Best practices for Remotion - Video creation in React
tools
Fully autonomous Pinterest pin production for Clarido. Auto-selects topics from blog gaps, generates content with pre-render validation, visual-reviews rendered PNGs via agent, publishes via Chrome automation, and emails results. No approval gates. Supports batch mode.
data-ai
Pull cross-channel GTM performance data from Meta Ads, Instagram, Pinterest, Blog, GA4, and Search Console — generate a unified report with analysis and recommendations.
business
End-to-end ad creative generation for Clarido — from performance analysis to rendered video variants. Use when creating new ad creatives, video ads, or marketing content.