An automated posts pipeline that lets an AI plan its own week
May 29, 20266 min read
A taco truck posts to Facebook and Instagram around nineteen times a day, every day, and a human touches none of it. The system that makes that happen is the most complete piece of automation I've built, and the architecture generalizes — the same skeleton runs the marketing for Blindando Sueños, the insurance SaaS.
I'll tell it through El Camioncito, the taco truck, because that's the implementation I documented end to end. Four moving parts that hand work to each other, plus a fifth thing sitting above all of them that's the actual reason I'm writing this post.
Here's the shape of it, interactive. Press Generar 3 posts to drop drafts at the factory, Postear ya to push them onto the train, and watch the cards walk left to right. The amber chips flip green when a post clears the reconciler. Nothing here talks to a network — it's the mental model, not the live system.
The factory
A cron job runs in-process at 05:00 America/Matamoros every morning and generates that day's drafts. The truck posts on a cadence grid — roughly 19 slots a day Monday through Saturday, spread from 7 AM to 10 PM at 40–50 minute intervals, fewer on Sundays since the truck closes at 3.
For each slot the factory calls gpt-5.2 to write the caption, feeding it a sliding window of the last 30 posts so it doesn't repeat a hook it used yesterday. The image is not generated per post — that would be slow, expensive, and off-brand. Instead it picks from an admin-curated pool: real photos uploaded through the panel, vision-classified on the way in, tagged with which post templates they suit. The factory queries that pool with anti-repetition logic and attaches a real WebP URL. The output is a row in the posts table with status='draft' and a scheduled_for timestamp in the future.
The factory is the only stage that touches a language model on the per-post path. Everything downstream is plumbing.
The train
Once the day's drafts exist, submitBatchToUploadPost() loads them onto the train. The train is upload-post.com — a third party that fans a single submission out to Facebook and Instagram so I don't have to maintain two Graph API integrations and their separate rate limits and review quirks.
Each draft goes up with its scheduled date attached. upload-post hands back a job_id — an async handle, because publishing isn't instant and I'm not going to block a cron tick waiting on it. The post flips to status='scheduled' and stores its job_id. The train's job is done the moment it has a receipt.
The reconciler
This is the stage that earns its keep. A separate cron fires every 15 minutes and polls each scheduled post's job_id: published, failed, or expired. On success it writes back the real Facebook post URL. On failure it decides whether to retry.
And retrying is where the interesting logic lives, because upload-post enforces a daily cap. The reconciler runs a cap-tracker that protects the morning. Posts scheduled before 7 AM get priority — that's the opening rush, the slot you don't want to miss. If a post hits a full cap, the tracker reschedules it to the next free slot within an 8-hour drift window, retrying up to 8 times. A post that hits the cap late in the day (after 5 PM) doesn't fight for a slot — it's expired immediately, because burning a retry on a 9 PM post risks starving tomorrow's 7 AM opening. The cap is a shared resource and the reconciler spends it deliberately.
State transitions are a small state machine: draft → scheduled → published, with failed and expired as terminal branches. Every transition is logged as structured JSON to stdout, so a journalctl grep reconstructs exactly what happened to any post.
The trash truck
The feed posts are permanent. The Stories aren't — they're ephemeral MP4s that exist to be published once and then forgotten. Keeping them around is just storage cost and clutter.
So there's a sweeper. Story videos are uploaded to Luna — my own CDN — through a separate API key scoped for 24-hour auto-deletion. After a Story publishes successfully, deleteFromLuna() removes it immediately rather than waiting for the TTL. The trash truck keeps the ephemeral lane actually ephemeral. It's the least glamorous stage and the one that quietly stops the storage bill from creeping.
The cocinero — where the AI plans its own week
Everything above is deterministic. Cron fires, captions get written, the train runs, the reconciler reconciles. The factory uses a model to write copy, but the decisions — when to post, how many, which template — are encoded in the cadence grid. That's fine for the daily drip. It's not fine for the multi-showcase posts: curated albums where several photos of one dish go out together as a carousel. Those need judgment. Which sets are fresh enough to feature? How many this week without flooding the feed? Spread across which days?
That judgment is made by a thing I call the cocinero — the cook — and it's a headless Claude Code agent on a systemd timer.
Every Monday at 06:30 (thirty minutes after the weekly batch finishes cooking its operational slots), a oneshot service wakes up and launches a Claude Code session with no human in the loop. It runs on my Max-plan OAuth session — no API key, which is the detail I find most satisfying: the same subscription I use interactively all day also drives an unattended weekly planner. The agent pulls the week's context from the backend — which photo sets exist, which are enabled, what's already scheduled, which days are closed — reasons about it, and emits a JSON plan: an array of {set_id, scheduled_for, count}. The plan is schema-validated, then POSTed back to the backend at /admin/sets-planner/run-week, which expands each entry into actual draft rows. From there the factory's normal machinery takes over: text generation, image attachment, train, reconciler.
The session is cached per ISO week, so a rerun is idempotent — it resumes the same reasoning rather than planning the week twice. And it respects the same closed-day calendar the rest of the system reads: no scheduling on closure days, honor the opening overrides.
That's the hook for me. Not "an AI writes the captions" — everyone does that now. It's that an AI agent makes the editorial calls a marketing person would make, on a schedule, unattended, and the rest of the pipeline treats its output exactly like any other draft. The cocinero plans; the factory builds; the train ships; the reconciler cleans up after.
Why this generalizes
The clients are wildly different — a taco truck, a lottery business, an insurance company — but the pattern doesn't care. Factory → train → reconciler → cleanup is a queue with a generation step in front and a self-healing step behind, and that describes most "do this thing reliably, many times, without supervision" problems. Swap the generator, swap the destination, keep the reconciler honest, and you can point it at almost anything.
The piece worth stealing isn't any single stage. It's the separation: a fast, deterministic pipeline that never blocks, with an expensive judgment call lifted out to its own scheduled agent that feeds the pipeline rather than living inside it. The cron jobs run every 5 to 15 minutes and must be cheap. The planning runs once a week and is allowed to be slow and smart. Keep those two clocks apart and the whole thing stays calm.
The demo above is the mental model with fabricated data. The real version has been posting tacos to two platforms, nineteen times a day, for months — and on Mondays, deciding for itself what to show.