One morning this month I asked for my posting schedule and counted twenty-seven posts waiting in Buffer across Facebook, Instagram and YouTube. I hadn't typed one of them in. This is the technical side of how that works, and most of it is about the parts built to say no.
The shape of it
Claude, Anthropic's model, is the agent. It drafts each week from a rules file and a facts table, builds the pictures and Shorts in code, and calls three Make scenarios as tools through Make's MCP server. Each scenario takes a few plain inputs and hands one post to Buffer, which publishes to my Facebook Page, Instagram and YouTube.
Images and videos live on my own site, because Buffer fetches media from a public link when the post publishes. Each file stays up until its post goes out.
Before any of it runs, the week comes to me as one markdown file. Nothing is queued until I approve it.
Guardrail 1: the scenario can only schedule
Every scenario starts with a filter. All three conditions have to pass, or the run stops at one operation and nothing reaches Buffer.
{
"name": "Known channel, at least 30 minutes out, video on merakislove.com",
"conditions": [[
{ "a": "{{1.channel_id}}", "o": "exist" },
{ "a": "{{var.input.scheduled_at}}", "b": "{{addMinutes(now; 30)}}", "o": "date:greater" },
{ "a": "{{var.input.video_url}}", "b": "https://merakislove.com/", "o": "text:startwith" }
]]
}
There is no route for "post now." Buffer receives mode: customScheduled with a dueAt, and the filter refuses any time less than 30 minutes away, so a bad clock or a wrong time zone can't turn into an instant post.
Guardrail 2: the caller never supplies an ID
The agent says facebook or instagram. Make turns that word into Buffer's channel ID on its own side.
{{switch(var.input.channel; "facebook"; "<facebook channel id>"; "instagram"; "<instagram channel id>")}}
Anything else comes out empty and fails the exist check. The match is exact, so Facebook with a capital F is refused too. I'd rather fix a capital letter than discover a post went to the wrong place.
Guardrail 3: the trailing slash
The media check is text:startwith "https://merakislove.com/", and the slash at the end matters. Without it, https://merakislove.com.example.org/anything.mp4 would pass. I tested exactly that address, and it was refused.
Guardrail 4: the key lives in Make
The two video routes call Buffer's GraphQL API from Make's HTTP module, authenticated with an API key stored in Make's keychain. I created the key in Buffer and typed it into Make myself. The scenario refers to it by its keychain reference, so the agent can run the route and read Buffer's reply without the key ever appearing in a prompt, a file or a chat.
The request itself is one mutation:
mutation Create($input: CreatePostInput!) {
createPost(input: $input) {
__typename
... on PostActionSuccess { post { id status dueAt channelService } }
... on InvalidInputError { message }
}
}
with variables shaped like this for an Instagram Reel:
{
"input": {
"channelId": "<from the switch above>",
"schedulingType": "automatic",
"mode": "customScheduled",
"dueAt": "2026-10-21T00:30:00.000Z",
"needsApproval": false,
"text": "<post text, JSON encoded>",
"assets": [{ "video": { "url": "https://merakislove.com/social/video/<file>.mp4" } }],
"metadata": { "instagram": { "type": "reel", "shouldShareToFeed": true, "isAiGenerated": true } }
}
}
The post text passes through Make's JSON transformer before it goes into the body, so quotes, backslashes and line breaks can't break the request. I tested all three.
One Make detail cost me a run. An HTTP module (version 4) created through Make's API needs stopOnHttpError, allowRedirects, shareCookies and requestCompressedContent set in its mapper. Leave them out and the run fails with "Validation failed for 4 parameter(s)."
Testing the fences
I don't trust a check until I've watched it fail.
For the picture route, I ran a copy with the Buffer step replaced by a step that does nothing. Two inputs should have passed and six should have been refused: a YouTube channel name, Facebook with a capital F, a date in the past, a time twelve minutes out, an image on another site, and an image over plain http. Two passed. Six stopped at one operation.
For the Facebook and Instagram video route, the test copy pointed at channel IDs that don't exist. A valid request could reach Buffer's checks without ever creating a post. Buffer answered NotFoundError, which proved the request was shaped correctly. Then I sent a deliberately invalid post type and got a schema error back, which proved Buffer's own validation was running. Both copies were deleted afterward.
Building the media
Screenshots that refuse broken pages
Pictures come from the live demo pages through Playwright and Chromium. The helper hides the chat button and floating call bar, scrolls to a CSS selector, and captures at 1600 by 900, 1200 by 675 or 1080 by 1350. My first version trusted that a file existed. Then a preview came back blank because a script chunk failed, and two days later a pricing page came back as unstyled text because its stylesheet never arrived. Now it listens for failures and refuses the picture:
def missing(kind, u):
if kind == "stylesheet" or (kind in ("script", "font", "image") and urlparse(u).netloc == host):
lost.append(f"{kind} {u}")
pg.on("requestfailed", lambda r: missing(r.resource_type, r.url))
pg.on("response", lambda r: missing(r.request.resource_type, r.url) if r.status >= 400 else None)
It also refuses a picture that comes out one flat color. I proved the guard on a local page with the stylesheet missing (refused) and present (written).
One demo counts down to the next tax deadline, so a screenshot taken today would show the wrong number on the day it posts. An init script moves the page's clock to the publish time:
(() => {
const R = Date, off = new R("2026-10-15T22:10:00Z").getTime() - R.now();
class D extends R {
constructor(...a) { if (a.length === 0) super(R.now() + off); else super(...a); }
static now() { return R.now() + off; }
}
window.Date = D;
})();
Shorts from page recordings
The same browser opens a demo at phone size (432 by 576 at 2.5x) and records frames through the DevTools screencast while a script scrolls along timed keyframes with easing. I measured each site's sticky header so the line being talked about never slides under it. ffmpeg places the recording in a 1080 by 1920 frame with a title, captions, a music bed and an end card. Buffer refuses a YouTube Short that is wider than it is tall, which I learned when my first 16:9 commercial came back with "Video must be vertical (portrait orientation) for YouTube Shorts."
Narration in my voice
One Short a week is narrated by a clone of my voice in ElevenLabs. Each take costs credits, so every take is generated once, with no retry loop. Then ffmpeg trims the silence at both ends, slows the tempo with rubberband while keeping the formants, and levels it with loudnorm=I=-17:TP=-1.5:LRA=9. faster-whisper (small.en, word timestamps on) listens back, and each caption is timed to the span of words it covers.
Cartoons with Kling
My AI Cookbook gets a cartoon owl tutor. Kling's image model draws the opening frame for 2 credits, and its video model animates a ten second 1080p clip for 100 credits, with voices generated from quoted lines in the prompt. Later episodes use the first frame as a character reference so the owl and the kid stay consistent. The agent can't hear, so it transcribes each clip to confirm the lines before adding speech bubbles and an end card built in HTML, and then asks me to listen once. Every job runs under a cap I set in advance. When one clip stalled for more than an hour, the agent reported it and did not resubmit.
What broke in week one
Blank and unstyled screenshots, fixed by the failure listener above.
A 16:9 video refused for Shorts. The vertical cut published instead.
Buffer's free plan holds ten scheduled posts per channel. The eleventh came back as a 403 with "You've hit the 10 scheduled post limit," nothing was created, and the scenario stores no incomplete runs, so nothing retried by itself.
My first Shorts went into the queue before I had watched them. That's why the approval file now comes first.
The full write-up, with the diagram and the reasoning behind each fence, is the canonical version linked above, and the demo sites these posts feature are at merakislove.com. If you've handled Buffer's limit or Make's HTTP quirks another way, tell me in the comments. I'd like to compare notes.
Top comments (0)