A To-Do Is a Tweet: Social Publishing With Approval Cards
By Arshad Ansari
I needed to schedule social posts. LinkedIn mostly, Facebook occasionally — announcements, links to things I'd written, the usual low-volume feed of someone who builds more than he posts.
The obvious move is a scheduling UI. A composer, a calendar view, a preview pane, a timezone dropdown. Every social tool ships one, and I've built enough internal admin pages to know exactly how that project goes: two weekends for the form, a third for the edge cases, and at the end of it, one more widget I have to remember exists.
Before starting, I looked at what a scheduled post actually is. Some copy. A time it should go out. A list of channels.
Then I looked at a Todoist task: a title, a due date-time, labels.
Same shape. The scheduler was already installed on my phone.
The task is the post
AEGIS already treats Todoist as the canonical task store — a sync flow mirrors it into Postgres every five minutes, and the GTD machinery works off that mirror. So the entire authoring interface for social publishing is this: write the copy as a task title, add a plain publish label, add a platform label per channel — linkedin, facebook — and set the due date-time to when it should go out.
That's it. There is no composer. Thursday, 6pm, two labels. A to-do is a tweet, give or take the platform.
SocialPublishFlow runs every five minutes on the Temporal worker. Each tick, it looks for tasks wearing the publish label that are due within a lookahead window. When it finds one, it does not post it.
It asks.
The part I didn't have to build
Everything in AEGIS that needs a human runs through one primitive. An interaction is a row in Postgres, a card in the active chat channel, and a Temporal workflow parked on a signal, waiting for a tap. Approvals, choices, free-text input, draft reviews, acknowledgements — five kinds, one mechanism. I've written about it before in one primitive, every interruption.
Posting to a public account is the most obvious approval gate in the whole system. So when SocialPublishFlow finds a due post, it raises an approval interaction, exactly the way the email flow does when it wants to send a reply and the infra flow does before touching a service. The card shows the copy and the channels. I tap approve, the workflow gets its signal, and only then does anything irreversible happen: the approved post becomes a social_outbox row, and the SocialConnector picks it up and delivers it.
Delivery goes through a self-hosted Postiz instance running on the same small Docker Swarm as everything else. Postiz owns the actual platform connections — the OAuth dances, the tokens, the API quirks. AEGIS never holds a LinkedIn credential; it talks to a service on my own network that does. A day later, SocialMetricsFlow pulls the analytics back from Postiz, so the numbers land in the same database as everything else the system knows.
What I want to point at is what's missing. There's no posts table with its own status machine. No bespoke approval endpoint. No new UI, no new notification path, no new way to say yes. The pending post is an interaction like any other, sitting in the same queue as a draft email and a flagged renewal. This is the most satisfying reuse of that primitive so far: an entire product category — social schedulers — collapsed into two flows, one connector, and a card format I already had.
What's actually hard
Three things, and only one of them is code.
Just-in-time versus scheduled-in-advance. SocialPublishFlow only looks a short window ahead of now. That's deliberate: the approval card lands close to the moment of publishing, when I still remember why I wrote the thing and can judge whether it's still worth saying. It's also safe — a post that stops making sense before its due time never even becomes a question.
The cost is that "schedule my whole launch calendar two weeks out" doesn't work by default. Those tasks sit in Todoist, visible to me but invisible to the flow, until their turn approaches. If I want to bless a fortnight of posts in one sitting, I have to widen the lookahead window on purpose, take the stack of cards, and approve them against a future I'm guessing at. It's a real trade-off between freshness of judgment and batch convenience. It's also reversible — the window is a config value, not an architecture — which is the kind of trade-off I can live with.
Per-platform payloads. I wanted the connector's contract to be "post this text to these platforms." No platform agrees to that contract. Each one has its own idea of required settings, and the delivery API wants them stated per platform on every submission. Postiz absorbs the worst of it — the tokens, the auth refreshes, the endpoint churn — but the abstraction still leaks upward: the connector ends up building a platform-specific settings block for every channel a post targets, whether the settings are interesting or not. I've stopped fighting it. A generic "post this" layer that pretends platforms are interchangeable is a lie you pay for later; a thin per-platform mapping is boring and honest.
The discipline. The third problem isn't technical. It's the standing temptation to add an auto-approve path. Every approval gate I've ever built develops this itch — this post type is safe, this label is trusted, skip the card just this once. And every route around a human gate eventually gets taken at the worst possible time.
The rule in AEGIS is flat: automation never posts to a public account without a human tap. Not because the flow is unreliable — because publishing is irreversible in the way that matters. A wrong card in chat costs me a dismissal; a wrong LinkedIn post costs something I can't tap to undo. The tap takes two seconds. I've kept it, and I intend to keep keeping it.
Where it goes
More platforms, as I actually need them — Postiz already speaks to more networks than I use. And something calendar-shaped for seeing what's queued, because reading a publishing schedule out of Todoist due dates works, in the sense that a spreadsheet works as a wiki. The authoring model stays; the visibility could be better.
One last thing I enjoy more than I probably should: the announcement for this post is, at the time of writing, a Todoist task with a publish label and a due date. Somewhere between me saving this file and you reading it, a card asked me if I was sure.
I was.
AEGIS lives at /aegis, and the code — approval cards and all — is at github.com/hikmahtech/aegis.
Building something data-heavy? Let's talk.