Schedule to social
Setup: fill these in
Replace every placeholder below before the first run.
| placeholder | what it is |
|---|---|
<YOUR_METRICOOL_BLOG_ID> | Your Metricool brand's blogId (from getBrandSettings) |
<YOUR_TIMEZONE> | IANA timezone for scheduling, e.g. America/Chicago |
<YOUR_POST_TIME> | Default daily posting time, e.g. 10:00 |
<YOUR_IG_HANDLE>, <YOUR_TIKTOK_HANDLE>, <YOUR_YOUTUBE_CHANNEL_ID> | The accounts connected in Metricool |
<YOUR_DRIVE_UPLOAD_FOLDER_ID> | A Google Drive folder shared "anyone with the link" (see the media hop) |
<YOUR_VIDEO_LIBRARY> | Where finished files are kept, e.g. ~/Videos/Finished |
<YOUR_STANDING_CTA> | Optional closing line you put on every caption |
<YOUR_VOICE_FILE> | Optional: path to a voice/style guide for captions |
Brand: blogId <YOUR_METRICOOL_BLOG_ID>, timezone <YOUR_TIMEZONE>.
Connected: Instagram (<YOUR_IG_HANDLE>), TikTok (<YOUR_TIKTOK_HANDLE>), YouTube (<YOUR_YOUTUBE_CHANNEL_ID>).
The media hop (the part that isn't obvious)
Metricool's API takes public URLs only. It cannot accept a local file. The working chain:
- Upload the MP4 to a Drive folder such as
Metricool Uploads (Claude)(<YOUR_DRIVE_UPLOAD_FOLDER_ID>). Share the folder "anyone with link" once; files inherit that, so no per-file sharing.cd <dir with the file> && gws drive files create \ --params '{"uploadType":"multipart","supportsAllDrives":true}' \ --json '{"name":"<name>.mp4","mimeType":"video/mp4","parents":["<YOUR_DRIVE_UPLOAD_FOLDER_ID>"]}' \ --upload "<file>" - Schedule with the direct-download URL:
https://drive.google.com/uc?id=<fileId>&export=download. The/viewlink fails with "Failed to normalize media". - Metricool copies the file to its own storage; the response's
mediaflips tostatic.metricool.com/.... - Delete the Drive copy once you see that. Storage never accumulates.
Claude usually cannot make Drive files public (the sandbox blocks it) and cannot edit its own permissions. That is why the shared folder exists. If a file ends up outside the folder, ask the user to move it rather than trying to share it.
Captions
If you keep a voice guide, read <YOUR_VOICE_FILE> first and match it. Otherwise use these defaults.
When the reel has a comment-keyword CTA (a keyword that triggers an automated DM or email), line 1 is the CTA:
- Line 1:
Comment <KEYWORD> and I'll send you <resource name> free 👇, keyword in caps, exactly as spoken in the reel's CTA clip. - Then the hook line and the 2-4 teaching lines below.
- Close with the standing CTA, if you use one.
- The keyword must match the reel's spoken CTA (
reel_ctain the video's state file). One keyword per video.
Format for reels without a keyword CTA:
- Line 1 is the hook. It's the only line showing in feed. Usually the same line the reel opens on.
- 2-4 short lines that teach one thing. Line breaks between them, not a paragraph.
- No hashtag spam. Few or none.
- Close with
<YOUR_STANDING_CTA>verbatim if you have one.
Write from what's actually in the reel, using the creator's words from the transcript, not generic advice. Short but educational: the viewer should learn one usable thing from the caption alone.
Generic examples of the shape:
"Posting every day = BURNOUT / Posting 3 great videos a week = GROWTH"
"Quick tip if you want better audio: put the mic closer, not the gain higher..."
Rendering
Captions are burned in: video-reels puts them on V3 as an alpha overlay clip, with the text-hook card on V2. Nothing to import, no burn-in checkbox.
- The user trims the reel. Both overlay tracks are independent clips, so trimming V1 does not move them. If the timing is re-cut, re-render that reel's captions rather than sliding them.
- Claude renders them through the Resolve API. Four reels take about 90 seconds.
out = os.path.expanduser("~/Movies/<video>-deliverables/reels-final")
os.makedirs(out, exist_ok=True)
project.DeleteAllRenderJobs()
for i in range(project.GetTimelineCount()):
t = project.GetTimelineByIndex(i+1)
if not t.GetName().startswith("Reel"):
continue
project.SetCurrentTimeline(t)
project.SetRenderSettings({"TargetDir": out, "CustomName": t.GetName().replace(" ", "_"),
"SelectAllFrames": True, "FormatWidth": 1080, "FormatHeight": 1920,
"ExportVideo": True, "ExportAudio": True})
project.SetCurrentRenderFormatAndCodec("mp4", "H264")
project.AddRenderJob()
Then start_rendering. Codec string is "H264", not "H.264", and the format is "mp4". Poll the output directory rather than the job status; the files appear as they finish. Expect ~40MB for 30 seconds.
3. Render is the point of no return for timing, so do any trimming first.
4. Spot-check the output with ffmpeg before uploading, one frame inside the hook window and one mid-reel:
ffmpeg -v error -ss 1.5 -i reel.mp4 -frames:v 1 hook.png -y
ffmpeg -v error -ss 8 -i reel.mp4 -frames:v 1 cap.png -y
Stack them into one proof sheet and actually look at it. This is the last place a missing hook card or a blank caption track can be caught, and after scheduling there is no delete.
Where the finished files go
Everything kept lands in <YOUR_VIDEO_LIBRARY>/, one folder per video:
<YOUR_VIDEO_LIBRARY>/<Video> YYYY-MM-DD/
Reels/ what posted. 1080x1920 H.264
Reels/Masters/ same cut, 60 Mbps archive
Timelines/ a .drt per timeline + the project .drp
Covers/ the approved Instagram reel covers, 1080x1920 JPG
Thumbnails/ the full-size options
YouTube/ the exported long-form, delivery + master
Posting schedule.txt what goes out, where, and when
The upload file: as high as Instagram's publishing API allows
Metricool posts through Meta's content-publishing API, which caps what it accepts. Upload the best file that fits inside those caps, made from the 60 Mbps master and not from Resolve's delivery render:
- H.264 High, yuv420p, 1080x1920, closed GOP, moov at the front (
+faststart) - video 24 Mbps max (the cap is 25), audio AAC 128 kbps at 48 kHz (Resolve writes 320 kbps, which is over)
- video and audio streams only. Resolve adds a timecode data track; drop it with
-dn
ffmpeg -i "Masters/<n>.mp4" -map 0:v:0 -map 0:a:0 -map_metadata -1 -dn \
-c:v libx264 -preset slow -crf 11 -maxrate 24M -bufsize 48M -pix_fmt yuv420p -profile:v high \
-g 48 -keyint_min 48 -sc_threshold 0 -flags +cgop \
-c:a aac -b:a 128k -ar 48000 -movflags +faststart "Reels/<n>.mp4"
At CRF 16 talking-head footage only used ~8 Mbps, so CRF 11 spends more of the budget. Run it in the background, since several reels take minutes at -preset slow. Encode one file at a time, not in parallel: many parallel encodes can fill the disk mid-write and truncate every file. Instagram still re-compresses on its side; this makes sure what it starts from is as clean as it allows.
Two renders per reel, not one. Same codec, same resolution, different bitrate:
| resolution | bitrate | for | |
|---|---|---|---|
| delivery | 1080x1920 H.264 | Resolve pass, then replaced by the upload file above | uploading |
| master | 1080x1920 H.264 | 60 Mbps via "VideoQuality": 60000 | archive |
Same two passes for the long-form at 1920x1080: ~900MB delivery, ~4.5GB master for a 10-minute video.
1080x1920 is the right resolution: Instagram serves 1080 wide regardless, and the reel crop is already oversampled from 4K. Don't offer ProRes or a bigger frame unless asked.
VideoQuality is sticky. Set it on every single pass.
Render settings live on the project, not the job. Leave VideoQuality out of a SetRenderSettings call and the render silently reuses whatever the previous one used. A long-form delivery pass that inherits 60000 from the reel masters comes out ~5x bigger than intended.
project.SetRenderSettings({..., "VideoQuality": 12000}) # delivery, ~12 Mbps
project.SetRenderSettings({..., "VideoQuality": 60000}) # master, ~60 Mbps
Never rely on the default for the delivery pass. And never trust the job listing: VideoQuality reads back as None from GetRenderJobList() even when it applied. Check the finished file:
ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of csv=p=0 out.mp4
Check free space before queueing a long-form pair. A 10-minute master at 60 Mbps is ~4.5GB.
Export the timelines into the folder too, so the edit is portable without the project database:
for i in range(project.GetTimelineCount()):
t = project.GetTimelineByIndex(i+1)
t.Export(os.path.join(d, t.GetName() + ".drt"), resolve.EXPORT_DRT)
resolve.GetProjectManager().ExportProject(project.GetName(), os.path.join(d, project.GetName() + ".drp"))
- Render straight into it. Set the Resolve
TargetDirto<that folder>/Reels, so nothing has to be moved later. - Rename off the Resolve timeline names.
Reel_2_-_my_best_tip.mp4becomes2. my best tip.mp4: numbered so they sort, readable at a glance. Same names for the masters and the .drt files, so the three folders line up. - Move, don't copy.
~/Movies/<video>-*holds working files (whisper JSON, SRTs, overlay movs, caption movs). Those stay put for re-renders; the finished exports leave. - Write
Posting schedule.txtafter scheduling, listing each reel's date, time and networks, so the user can see what is already out the door without opening Metricool.
Mention disk space only if a render actually fails for space, or if a long-form pair clearly won't fit.
Covers (Instagram only)
Every reel gets a custom Instagram cover before it's scheduled. Run the reel-covers skill after the captions are approved and before scheduling. It hands back one approved JPG per reel in <Video>/Covers/. Show the covers at the same gate as the captions and slots, so everything is approved in one pass.
Upload each cover through the same Drive hop as the video (same folder <YOUR_DRIVE_UPLOAD_FOLDER_ID>, mimeType: image/jpeg), and pass it as videoThumbnailUrl using the uc?id=...&export=download form. Delete the Drive copy once Metricool has its own copy, the same as with the video.
Scheduling
One post per reel, all three networks, cover included. The cover only matters on Instagram. TikTok ignores it on a personal account, and Shorts only shows it on a verified channel. Don't split the post.
"providers":[{"network":"instagram"},{"network":"tiktok"},{"network":"youtube"}],
"media":["https://drive.google.com/uc?id=<video id>&export=download"],
"videoThumbnailUrl":"https://drive.google.com/uc?id=<cover id>&export=download",
"instagramData":{"type":"REEL","showReelOnFeed":true,"isAiGenerated":false},
"tiktokData":{"privacyOption":"PUBLIC_TO_EVERYONE","title":"<short title>"},
"youtubeData":{"type":"short","privacy":"public","madeForKids":false,"title":"<title, <100 chars>"}
- Adding a cover to a post that's already scheduled:
updateScheduledPostwith the post's full original fields (text, providers, media, all network data, date) plusvideoThumbnailUrl. It overwrites and has no undo, so copy the fields fromgetScheduledPostsverbatim. Never replace an existingvideoThumbnailUrlorvideoCoverMilliseconds: a post that has either already has a chosen cover. - If one update fails with "Failed to normalize media" while the others worked, the fetch from Drive failed that once. The post is left unchanged. Upload the file again (new id) and retry. Retrying the same URL won't help.
- Then check with
getScheduledPoststhat every post'svideoThumbnailUrlnow starts withstatic.metricool.com, and delete the Drive copies. - YouTube requires a title and
madeForKids. TikTok wants a short title. autoPublish: truelets Metricool post it automatically.
When to schedule (default rule, adjust to taste)
Start the day after today. One post per day. If a day already has content, move to the next free day.
day = tomorrow
for each post in order:
while day already has a scheduled post: day += 1
schedule at <YOUR_POST_TIME> local on day
day += 1
<YOUR_POST_TIME>in<YOUR_TIMEZONE>is the default posting time.- Always call
getScheduledPostsfirst, covering tomorrow through tomorrow + (number of posts + 14 days), so you can see the gaps. Skipping this check can land a post on an already-taken slot. - A day counts as taken if anything is scheduled that day, drafts included.
- Don't bunch several posts on one day, and don't leave holes: the point is a steady daily run starting immediately.
- Never put a test label in the caption text. No "TEST DRAFT", no "safe to delete". The caption is the finished copy, always, even on a test run; the user may want to keep it.
Mark test posts with
draft: true+autoPublish: false(drafts are visually distinct in the planner) and hand over theplannerUrlplus the post id so it can be deleted in one click.
Long-form YouTube (the main video, not a Short)
Recommended: don't send large long-form files through Metricool. Upload them in YouTube Studio. A ~1GB export sent through Metricool can be stored truncated mid-transfer. Resolve puts the MP4 index (moov) at the end of the file, so a truncated copy is unreadable, and YouTube reports "processing abandoned" after it has already "published". Metricool may not report an error. Hand the user the file, title, description and thumbnail to paste into Studio, rendered in 4K (Studio has no size problem). Everything below is kept for reference.
If Metricool is used for a big file, check the stored copy before trusting it:
curl -sI <static.metricool.com url>content-length must match the local file, andffprobe <url>must open it.
Metricool publishes full videos too, with the thumbnail and title from video-thumbnails.
"providers":[{"network":"youtube"}],
"youtubeData":{"type":"video","title":"<40-65 chars>",
"privacy":"public","madeForKids":false,
"category":"EDUCATION","tags":["<topic tag>","<topic tag>"]},
"videoThumbnailUrl":"https://drive.google.com/uc?id=<thumb file id>&export=download"
- The post
textbecomes the video description. Write it properly: the hook line, a short what-you'll-learn, your main link or CTA, then timestamps if the video has clear sections (the paper-edit transcript gives you these for free). videoThumbnailUrlmust be jpg/png and goes through the same Drive hop as the video. 1920x1080 PNG is fine.- Custom thumbnails only display on a verified channel. If the channel isn't verified, YouTube silently uses an auto-frame. Check before promising it landed.
- Sending a thumbnail where it doesn't apply rejects the whole request with
VIDEO_THUMBNAIL_NOT_APPLICABLE, so don't attach one speculatively. - Large files (over ~100MB) need a different Drive link (and past ~600MB, see the warning above). Drive answers
uc?id=...&export=downloadwith a "can't scan for viruses" page instead of the file, and Metricool fails with "Failed to normalize media". Usehttps://drive.usercontent.google.com/download?id=<fileId>&export=download&confirm=t, which returns the file itself. Check it first withcurl -sIL <url> | grep -i content-length. - Long-form and Shorts are separate posts. Don't put a 10-minute video in a post that also has
type: "short".
After scheduling: log every reel (optional)
If you track reel performance, add one row per scheduled reel to a tracker file (date, batch, reel, format, hook style, length, CTA, punch-ins) and fill in stats 7 days later. This is how the edits improve from real numbers.
The gate
Captions ship clean. Write every caption as if it is going live, including on test runs.
Show every caption and every time before scheduling. Once autoPublish is true and the date passes, it posts by itself. There is no undo in the API.
After scheduling: confirm each post's media is static.metricool.com, delete the Drive copies, and hand over the planner links.
There is no delete in the API. getScheduledPosts, createScheduledPost and updateScheduledPost are all you get. A post Claude creates can only be removed by the user, in the planner. So:
- Never create a post you expect to throw away. Test against a real caption on a real free slot, or not at all.
- When asked to delete one, save its caption to a local drafts folder first, then give the
plannerUrland say it takes one click. - Check what the post actually contains before agreeing it is a duplicate. Two drafts that look like "old versions" may hold a different reel entirely.
Related
video-reels: makes the reels and the SRTsreel-covers: makes the Instagram coversyoutube: the conductor; this is its last stage