I wanted to post a reel every day on two Instagram pages: my own tech page, and a page of short clips from spiritual talks. I did not want to spend an hour a day editing, exporting, uploading and remembering to hit publish.
So I built reel-autopilot. It makes the reel, checks it, asks me to approve it on Telegram, schedules it and posts it. In its first week it put out 17 reels across the two accounts while I did other things.
I expected the AI part to be the hard part. It wasn't. The hard part was everything around it: a laptop that goes to sleep, Instagram drafts that expire, two programs editing the same queue, and a git push that once hung for two hours.
This post walks through how it works and the problems that actually took the time.
The Pipeline
Every reel goes through the same five steps:
The only step that needs me is the third one, and it takes about ten seconds.
Step 1: Making the Reel
The renderer is a page that draws one frame of the reel from a single number: the time. Playwright opens it in headless Chromium, steps through every frame at 30 fps, captures each one, and ffmpeg stitches them into an MP4.
That design means a reel is just code and data. There is no timeline editor and no project file. To make a new one, Claude Code writes a spec, the renderer turns it into frames, and the result is the same every time.
I run Claude Code in headless mode with a playbook that explains how reels are made, a fixed list of tools it is allowed to use, and file edits accepted automatically. Web search is only switched on for tech reels, where it needs to look things up.
When I tap Fix in Telegram and type a note like "slower cut on the second line", the note goes straight back to Claude with the same playbook.
Step 2: Checks Before a Human Sees It
A reel that is the wrong size, too quiet or too long should never reach my phone. So before anything is sent for approval, the video goes through a set of checks, all read from ffmpeg's own report:
If anything fails, Claude gets the list of problems and re-renders. It gets up to three attempts in total. Only a reel that passes everything is sent to me.
if (info["width"], info["height"]) != (1080, 1920):
problems.append(f"size is {info['width']}x{info['height']}, must be 1080x1920")
if not info["fps"] or abs(info["fps"] - 30) > 0.5:
problems.append(f"frame rate is {info['fps']}, must be 30")
...
if lufs is None or not -15.0 <= lufs <= -13.0:
problems.append(f"loudness is {lufs} LUFS, must be -14 (±1)")This is the single most useful part of the system. Every reel I see is at least technically correct, so my ten seconds of review are spent on whether it is good, not on whether it is broken.
Step 3: The Queue Lives in Git
Here is where it gets interesting. Three things need to share the posting queue:
I did not want to run a database for a list of twenty reels. So the queue is a single JSON file, data/schedule.json, committed to the repo. The Mac pushes changes, the cloud job pulls them, and the git history is a free audit log of every state change.
It holds ids, slots, captions and states. It never holds a secret, so the cloud job can read it safely.
Sharing a file through git sounds simple. It took three fixes to make it reliable.
Writes are atomic. The file is written to a temporary file first and then renamed over the real one, so a crash halfway through a write can never leave a broken JSON file behind.
Local programs take turns. The bot, the daily job and the draft job can all run at the same moment on the Mac. Their git steps share a file lock, so only one of them touches git at a time.
A failed push retries once. If the cloud job pushed first, the Mac pulls with rebase and pushes again.
The Two-Hour Push
On 15 October, one git push hung for two hours. It held the git lock the whole time, which blocked the daily build, so no new reels were made that day.
The fix seemed obvious: give every git command a timeout. I did, and the next day a push hung for 16 minutes anyway.
The reason was subtle. When the timeout fired, I killed git. But git had started ssh, and ssh was still alive, holding the output pipe open. My code was waiting for that pipe to close, and it never did.
The real fix kills the whole process group, not just git:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
start_new_session=True, env={**os.environ, "GIT_SSH_COMMAND": _SSH})
try:
out, err = proc.communicate(timeout=120)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)start_new_session=True puts git and everything it starts into its own process group, and killpg takes the whole group down at once. ssh itself is also told never to wait for a prompt and to give up on a dead connection, so it should not hang in the first place.
That is the kind of bug that never shows up in a demo. It only shows up when something runs every day for a week.
Step 4: Instagram Drafts Expire
Instagram's API, through Composio, can upload a reel as a draft and publish a draft. It cannot schedule one for later. And a draft only lives for about 24 hours.
That shaped the whole design:
This split means the reels post on time even if my laptop is closed at 8:30 in the evening. The one rule is that the Mac has to be on at some point on the posting day, to create the draft.
Step 5: Posting Exactly Once
Two things can try to publish the same reel: the cloud job, and the bot on my Mac as a backup. Networks fail, jobs run late, and GitHub's scheduler can start a job several minutes after its time.
So posting is built to be safe to repeat:
Scheduling the Mix
The two accounts post at different times, and the spiritual page follows a fixed pattern of two clips from one speaker, then one from another. The scheduler keeps that ratio across every reel already in the queue, not just the next one. Reels I make by hand jump to the next free day without breaking the pattern.
For a while, alternate days post in the morning instead of the evening, so I can compare which time does better.
One Small Extra: Comment for the Link
On tech reels I often mention a repo. If someone comments "repo", the system sends them the link in a private reply. It runs every ten minutes, through the same Composio connection.
What I Took Away From It
The whole thing is about 1,700 lines of Python, four launchd jobs on the Mac, a Telegram bot and one GitHub Actions workflow, all on free tiers. And every evening, a reel goes out without me thinking about it.
You can see what it posts on my Instagram.