Scheduling Instagram Posts with Claude Browser Automation (Because Meta's API Is a Nightmare)
Built a Claude Code skill to schedule Instagram posts via browser automation after the Meta Business API turned out to require business verification and existing MCP servers didn't work.
Scheduling Instagram Posts with Claude Browser Automation (Because Meta's API Is a Nightmare)
I just spent the last few days building a Claude Code skill that lets me schedule Instagram posts directly from my content workflow. The end result works great—I can tell Claude "schedule this image to Instagram for tomorrow at 9am" and it actually does it. But getting there involved fighting with Meta's business verification process, discovering that existing MCP servers are broken, and ultimately resorting to browser automation with AppleScript to upload files because React-based file pickers are apparently immune to normal automation tools.
It's slow. It's hacky. It absolutely works. And honestly? I'm kind of proud of how ridiculous the solution ended up being.
The Problem: Meta's API Requires Business Verification
Here's what I wanted: automated Instagram posting for my running blog. I write product reviews, take photos, and want to share them on Instagram without manually opening the app and tapping through the UI every single time. Seems reasonable, right?
So I looked at the official Meta Graph API. Turns out you need "business verification" to access the Instagram content publishing endpoints. Business verification means sending Meta your business documents, waiting for manual review, and probably sacrificing a small goat to the algorithm gods. For a solo developer running a personal blog? Not happening.
Fine, I thought. I'll use an MCP server—those are supposed to wrap APIs and make them easy to use with Claude. Except the existing Instagram MCP servers I found either don't support scheduling, require the same business verification, or are just straight-up broken. One of them claimed to work but threw authentication errors on every request. Another one required OAuth tokens that expired every 60 days and had to be refreshed manually.
At this point I was already annoyed enough that I decided to go nuclear: browser automation. If I can't use the API, I'll just control the browser like a human would and make it click the buttons for me.
The Solution: Claude in Chrome Browser Automation
I'm already using Claude Code's browser automation tools (via the Claude in Chrome extension) for other projects, so I knew it could interact with web pages. Meta Business Suite (the web interface for managing Instagram business accounts) has a full posting UI with image uploads, captions, scheduling, and all the features I need.
So the plan was simple: navigate to the Meta Business Suite composer, upload the image, paste the caption, set the schedule, and click publish. Claude would automate the whole thing by simulating what I'd do manually.
Except it wasn't simple at all.
First Obstacle: React-Based File Pickers Don't Play Nice with Automation
The first wall I hit was image uploads. Meta Business Suite uses React-based file input components, and those do NOT work with normal browser automation tools.
I tried using Claude's upload_image tool. Didn't work—the file picker dialog would open, but the tool couldn't interact with it.
I tried using form_input to set the file path directly. Nope—React's synthetic event system doesn't recognize that as a real user interaction, so it just ignores the input entirely.
I tried JavaScript injection to manipulate the DOM and set the file input value manually. React laughed at me and continued doing nothing.
Turns out Meta's file picker is a React component that only responds to actual native OS file picker interactions. It's not enough to set a value or trigger an event—you have to actually open the file picker and select a file like a human would.
Which is when I remembered that macOS has AppleScript, and AppleScript can control the native file picker.
The AppleScript Hack: Automating the Native File Picker
Here's the solution I landed on: use Claude's browser automation to click the "Add photo/video" button (which opens the native macOS file picker dialog), then immediately use AppleScript to navigate the file picker, select the image file, and click "Open."
The AppleScript looks like this:
osascript -e '
tell application "System Events"
tell process "Google Chrome"
set frontmost to true
delay 2
keystroke "g" using {command down, shift down}
delay 2
keystroke "/full/path/to/image/directory/"
delay 0.5
key code 36
delay 4
keystroke "filename.jpg"
delay 1.5
set theWindows to every window
repeat with aWindow in theWindows
try
tell aWindow
set theSheets to every sheet
repeat with aSheet in theSheets
try
tell aSheet
click button "Open"
return "Clicked Open button"
end tell
end try
end repeat
end tell
end try
end repeat
end tell
end tell'
Let me explain what this monstrosity does, because the timing and sequencing is critical:
- Wait 2 seconds for the file picker to fully open (if you don't wait, AppleScript triggers Cmd+Shift+G in the browser instead of the file picker, which is useless)
- Press Cmd+Shift+G to open the "Go to folder" dialog in the macOS file picker
- Type the full directory path (e.g.,
/Users/nick/images/) - Press Return (key code 36) to navigate to that folder
- Wait 4 seconds for the folder to fully load—this delay is critical because if you try to select a file before the folder loads, AppleScript just selects nothing and silently fails
- Type the filename to select it (e.g.,
image.jpg) - Iterate through all windows and sheets to find and click the "Open" button (more robust than hardcoding
sheet 1 of window 1, which breaks if window order changes)
The delays are non-negotiable. If you don't wait long enough, the whole thing falls apart. I learned this the hard way after about 20 failed attempts where the file picker would just stay open with no file selected, and AppleScript would return "success" while having done absolutely nothing.
For carousels (multiple images), I just replace the filename selection step with keystroke "a" using {command down} to select all files in the directory. Then all the images upload at once.
Second Obstacle: JavaScript Can't Inject Caption Text Either
Next problem: entering the caption text. My first instinct was to use JavaScript to set innerHTML or textContent on the contenteditable caption field. Should be easy, right?
Nope. Meta's React-based composer doesn't recognize DOM manipulation. You can inject text into the field, and it'll look like it's there, but when you click "Schedule," the post goes out with "This post contains no text." React's state management has no idea you put anything in the field because you didn't trigger the right synthetic events.
So I did what any reasonable developer would do: clipboard paste.
I use pbcopy to copy the caption text to the macOS clipboard, then have Claude click on the caption field and press Cmd+V to paste it. That React recognizes as legitimate user input, and the text actually persists through to the scheduled post.
printf 'Your caption text here with newlines\n\nSecond paragraph...' | pbcopy
Then in Claude:
mcp__claude-in-chrome__computer action=left_click coordinate=[x, y] # Click caption field
mcp__claude-in-chrome__computer action=key text="cmd+v" # Paste from clipboard
This is absurdly hacky, but it works 100% of the time, which is more than I can say for the "correct" approach.
Third Obstacle: Date and Time Pickers Are Spinbuttons
Meta's scheduling UI uses spinbutton controls for date and time. These are not normal text inputs. You can't just click and type a time like "09:00" and have it work. If you try, it appends instead of replaces, so you end up with "10:2309:00" or some other nonsense.
The correct approach (which I figured out through trial and error):
For dates:
- Triple-click the date field to select all
- Type the date in
d/m/yyyyformat (e.g.,16/2/2026) - Press Return to open the calendar picker
- Click the highlighted date to confirm
For times:
- Click the hour component specifically (not the whole field)
- Press Backspace twice to fully delete the old value
- Type the new hour (e.g.,
09) - Click the minute component
- Press Backspace twice again
- Type the new minute (e.g.,
00)
If you skip the double Backspace, it just appends and you get garbage. If you click the whole field instead of the specific component, it doesn't focus correctly and ignores your input. Meta's UI is extremely particular about interaction order.
Instagram's Scheduling Validation: 20 Minutes to 29 Days
One more fun fact: Instagram enforces a scheduling window of 20 minutes to 29 days from the current time. If you try to schedule outside that range, you get a validation error and the post won't schedule.
This means:
- Minimum: current time + 20 minutes
- Maximum: current time + 29 days
I learned this by trying to schedule a post for "today at 9:00" when it was already 10:30, and Instagram politely informed me that I'm an idiot who doesn't understand linear time.
The solution is to always verify the scheduled time is at least 20 minutes in the future before clicking "Schedule." Easy fix, but annoying to discover the hard way.
What the Workflow Looks Like Now
Here's the actual end-to-end workflow the skill automates:
- Navigate to Meta Business Suite composer (
https://business.facebook.com/latest/composer/) - Wait for page load (3 seconds—gotta let React do its thing)
- Paste caption first (via clipboard) into the shared "Text" field
- Upload image via AppleScript:
- Click "Add photo/video" button
- Wait 2 seconds for file picker to open
- Run AppleScript to navigate to image directory, select file, and click "Open"
- Wait 3 seconds for upload to complete
- Enable Instagram customization:
- Click "Customise post for Facebook and Instagram" toggle
- Click the "Instagram" tab to verify caption carried over
- Set scheduling:
- Scroll to "Scheduling options"
- Enable "Set date and time" toggle
- Triple-click date field, type date in
d/m/yyyy, press Return, click highlighted date - Click hour component, Backspace twice, type new hour
- Click minute component, Backspace twice, type new minute
- Verify time is at least 20 minutes in the future
- Make Instagram-only (optional):
- Scroll back to top
- Click "Post to" dropdown
- Uncheck "Facebook"
- Leave only "Instagram" checked
- Click "Schedule" and wait for confirmation
The whole thing takes about 15-20 seconds per post, which is slower than an API call would be, but way faster than doing it manually. And unlike the API, I don't need business verification or OAuth tokens or any of that nonsense.
The real magic? Batch processing. I can prepare a week's worth of product review posts, organize the images in a directory, and then tell Claude: "Schedule these five posts to Instagram—one per day at 9am starting tomorrow." Claude runs through the workflow five times in a row, each time with a different image and incrementing the date. What would take me 30+ minutes of manual clicking and typing happens in under two minutes while I go make coffee.
Sure, it's not instant like an API would be, but I can queue up an entire content calendar in the time it used to take me to schedule a single post manually.
The AppleScript Accessibility Permission Dance
One more gotcha: AppleScript needs "Accessibility" permissions to control the file picker. If you don't have that enabled, AppleScript fails with error -25211 and refuses to explain why.
To fix:
- System Settings → Privacy & Security → Accessibility
- Enable access for Terminal (or whatever app is running
osascript) - Sometimes you need to remove and re-add it after macOS updates
This is a one-time setup thing, but it's annoying enough that I documented it in the skill because I know I'll forget and waste 20 minutes debugging it again in six months.
Why This Solution Is Actually Pretty Great
Yeah, it's slower than an API. Yeah, it's hacky. Yeah, I'm using AppleScript to click buttons in a file picker like it's 2005. But here's why I'm genuinely happy with this:
No business verification required. I can use this right now without waiting weeks for Meta to approve my blog as a legitimate business.
No OAuth token refresh. I don't have to remember to regenerate tokens every 60 days or deal with expired credentials breaking my automation.
It uses the same UI I use manually. If Meta changes their API (which they do constantly), my automation breaks. If they change the web UI, my automation might break, but probably not—and if it does, I can fix it by updating coordinates or selectors, not by rewriting authentication flows.
Claude handles the complexity. I documented the workflow in the skill file, and now I can just tell Claude "schedule this image to Instagram for tomorrow at 9am" and it handles all the timing, clicking, pasting, and verification. I don't have to remember the 15-step process or the critical delays. Claude does.
It actually works. This is the big one. I tried the "correct" approach (Meta's API) and it didn't work without business verification. I tried existing MCP servers and they were broken or incomplete. Browser automation with AppleScript is janky and slow, but it works, which puts it ahead of every other option.
Lessons Learned
The biggest lesson here is that the "right" solution isn't always the working solution. Meta's Graph API is the "right" way to post to Instagram programmatically, but it's gated behind business verification that I don't want to deal with. AppleScript controlling a file picker is objectively ridiculous, but it's the only thing that actually solved the problem.
The second lesson is that delays matter more than you think. Half of my debugging time was spent figuring out that I needed to wait 2 seconds after clicking "Add photo/video" before running AppleScript, and another 4 seconds after navigating to a folder before selecting files. Modern UIs are asynchronous and optimistic, which means you can't just fire commands as fast as possible and expect them to work.
The third lesson is that React-based UIs are surprisingly hostile to automation. DOM manipulation doesn't work because React's synthetic event system ignores it. You have to simulate actual user interactions (keyboard input, mouse clicks, clipboard paste) to trigger the right event handlers. It's more brittle than server-side automation, but it's the only option when APIs aren't available.
If You Want to Try This Yourself
The skill is part of my barefoot running blog project, but the Instagram publishing logic could easily be extracted and reused. The core components are:
- Claude in Chrome extension for browser automation
- AppleScript for file picker interaction (macOS only—Windows would need AutoHotkey or similar)
- Clipboard paste for text injection
- Careful timing and delays to let React finish loading
You'd need to adapt the button coordinates and selectors for your specific Meta Business Suite layout, but the core workflow should be the same.
Fair warning: this is very much "works on my machine" territory. Meta could change their UI tomorrow and break everything. But for now, it's letting me schedule Instagram posts from my content workflow without touching the mobile app or waiting for business verification, which is exactly what I needed.
And if Meta ever approves my business verification request? Cool, I'll switch to the API. But I'm not holding my breath.
Related posts:
- I Built a WordPress Plugin for Claude Code - Another automation solution for when official APIs are too much hassle
- I Built a Meta-Plugin for Claude Code - How I'm building skills that generate skills
Questions or war stories about fighting with Meta's APIs? Find me on X/Twitter, LinkedIn, or email me at nicholastwinder@gmail.com. I promise I actually read messages, and I'd love to hear if you've found better solutions to this nonsense.
